diff --git a/src/storage/v2/constraints.hpp b/src/storage/v2/constraints.hpp
new file mode 100644
index 000000000..a7364c40d
--- /dev/null
+++ b/src/storage/v2/constraints.hpp
@@ -0,0 +1,75 @@
+#pragma once
+
+#include <optional>
+#include <vector>
+
+#include "storage/v2/id_types.hpp"
+#include "storage/v2/vertex.hpp"
+#include "utils/result.hpp"
+#include "utils/skip_list.hpp"
+
+namespace storage {
+
+struct Constraints {
+  std::vector<std::pair<LabelId, PropertyId>> existence_constraints;
+};
+
+struct ExistenceConstraintViolation {
+  LabelId label;
+  PropertyId property;
+};
+
+/// Adds a unique constraint to `constraints`. Returns true if the constraint
+/// was successfuly added, false if it already exists and an
+/// `ExistenceConstraintViolation` if there is an existing vertex violating the
+/// constraint.
+///
+/// @throw std::bad_alloc
+/// @throw std::length_error
+inline utils::BasicResult<ExistenceConstraintViolation, bool>
+CreateExistenceConstraint(Constraints *constraints, LabelId label,
+                          PropertyId property,
+                          utils::SkipList<Vertex>::Accessor vertices) {
+  if (utils::Contains(constraints->existence_constraints,
+                      std::make_pair(label, property))) {
+    return false;
+  }
+  for (const auto &vertex : vertices) {
+    if (!vertex.deleted && utils::Contains(vertex.labels, label) &&
+        vertex.properties.find(property) == vertex.properties.end()) {
+      return ExistenceConstraintViolation{label, property};
+    }
+  }
+  constraints->existence_constraints.emplace_back(label, property);
+  return true;
+}
+
+/// Removes a unique constraint from `constraints`. Returns true if the
+/// constraint was removed, and false if it doesn't exist.
+inline bool DropExistenceConstraint(Constraints *constraints, LabelId label,
+                                    PropertyId property) {
+  auto it = std::find(constraints->existence_constraints.begin(),
+                      constraints->existence_constraints.end(),
+                      std::make_pair(label, property));
+  if (it == constraints->existence_constraints.end()) {
+    return false;
+  }
+  constraints->existence_constraints.erase(it);
+  return true;
+}
+
+/// Verifies that the given vertex satisfies all existence constraints. Returns
+/// nullopt if all checks pass, and `ExistenceConstraintViolation` describing
+/// the violated constraint otherwise.
+[[nodiscard]] inline std::optional<ExistenceConstraintViolation>
+ValidateExistenceConstraints(Vertex *vertex, Constraints *constraints) {
+  for (const auto &[label, property] : constraints->existence_constraints) {
+    if (!vertex->deleted && utils::Contains(vertex->labels, label) &&
+        vertex->properties.find(property) == vertex->properties.end()) {
+      return ExistenceConstraintViolation{label, property};
+    }
+  }
+  return std::nullopt;
+}
+
+}  // namespace storage
diff --git a/src/storage/v2/storage.cpp b/src/storage/v2/storage.cpp
index 682cbe8ea..174805358 100644
--- a/src/storage/v2/storage.cpp
+++ b/src/storage/v2/storage.cpp
@@ -580,7 +580,8 @@ EdgeTypeId Storage::Accessor::NameToEdgeType(const std::string &name) {
 
 void Storage::Accessor::AdvanceCommand() { ++transaction_.command_id; }
 
-void Storage::Accessor::Commit() {
+[[nodiscard]] std::optional<ExistenceConstraintViolation>
+Storage::Accessor::Commit() {
   CHECK(is_transaction_active_) << "The transaction is already terminated!";
   CHECK(!transaction_.must_abort) << "The transaction can't be committed!";
 
@@ -589,6 +590,23 @@ void Storage::Accessor::Commit() {
     // it.
     storage_->commit_log_.MarkFinished(transaction_.start_timestamp);
   } else {
+    // Validate that existence constraints are satisfied for all modified
+    // vertices.
+    for (const auto &delta : transaction_.deltas) {
+      auto prev = delta.prev.Get();
+      if (prev.type != PreviousPtr::Type::VERTEX) {
+        continue;
+      }
+      // No need to take any locks here because we modified this vertex and no
+      // one else can touch it until we commit.
+      auto validation_result =
+          ValidateExistenceConstraints(prev.vertex, &storage_->constraints_);
+      if (validation_result) {
+        Abort();
+        return *validation_result;
+      }
+    }
+
     // Save these so we can mark them used in the commit log.
     uint64_t start_timestamp = transaction_.start_timestamp;
     uint64_t commit_timestamp;
@@ -622,6 +640,8 @@ void Storage::Accessor::Commit() {
   if (storage_->gc_config_.type == StorageGcConfig::Type::ON_FINISH) {
     storage_->CollectGarbage();
   }
+
+  return std::nullopt;
 }
 
 void Storage::Accessor::Abort() {
diff --git a/src/storage/v2/storage.hpp b/src/storage/v2/storage.hpp
index e18fd3536..c87e361d8 100644
--- a/src/storage/v2/storage.hpp
+++ b/src/storage/v2/storage.hpp
@@ -4,6 +4,7 @@
 #include <shared_mutex>
 
 #include "storage/v2/commit_log.hpp"
+#include "storage/v2/constraints.hpp"
 #include "storage/v2/edge.hpp"
 #include "storage/v2/edge_accessor.hpp"
 #include "storage/v2/indices.hpp"
@@ -215,7 +216,10 @@ class Storage final {
 
     void AdvanceCommand();
 
-    void Commit();
+    /// Commit returns `ExistenceConstraintViolation` if the changes made by
+    /// this transaction violate an existence constraint. In that case the
+    /// transaction is automatically aborted. Otherwise, nullopt is returned.
+    [[nodiscard]] std::optional<ExistenceConstraintViolation> Commit();
 
     void Abort();
 
@@ -252,15 +256,36 @@ class Storage final {
     return indices_.label_property_index.IndexExists(label, property);
   }
 
+  /// Creates a unique constraint`. Returns true if the constraint was
+  /// successfuly added, false if it already exists and an
+  /// `ExistenceConstraintViolation` if there is an existing vertex violating
+  /// the constraint.
+  ///
+  /// @throw std::bad_alloc
+  /// @throw std::length_error
+  utils::BasicResult<ExistenceConstraintViolation, bool>
+  CreateExistenceConstraint(LabelId label, PropertyId property) {
+    std::unique_lock<utils::RWLock> storage_guard(main_lock_);
+    return ::storage::CreateExistenceConstraint(&constraints_, label, property,
+                                                vertices_.access());
+  }
+
+  /// Removes a unique constraint. Returns true if the constraint was removed,
+  /// and false if it doesn't exist.
+  bool DropExistenceConstraint(LabelId label, PropertyId property) {
+    std::unique_lock<utils::RWLock> storage_guard(main_lock_);
+    return ::storage::DropExistenceConstraint(&constraints_, label, property);
+  }
+
  private:
   void CollectGarbage();
 
   // Main storage lock.
   //
   // Accessors take a shared lock when starting, so it is possible to block
-  // creation of new accessors by taking a unique lock. This is used when
-  // building a label-property index because it is much simpler to do when there
-  // are no parallel reads and writes.
+  // creation of new accessors by taking a unique lock. This is used when doing
+  // operations on storage that affect the global state, for example index
+  // creation.
   utils::RWLock main_lock_{utils::RWLock::Priority::WRITE};
 
   // Main object storage
@@ -272,6 +297,7 @@ class Storage final {
   NameIdMapper name_id_mapper_;
 
   Indices indices_;
+  Constraints constraints_;
 
   // Transaction engine
   utils::SpinLock engine_lock_;
diff --git a/tests/benchmark/storage_v2_gc.cpp b/tests/benchmark/storage_v2_gc.cpp
index aafce71c9..bb7787cd3 100644
--- a/tests/benchmark/storage_v2_gc.cpp
+++ b/tests/benchmark/storage_v2_gc.cpp
@@ -43,7 +43,7 @@ void UpdateLabelFunc(int thread_id, storage::Storage *storage,
         << "Vertex with GID " << gid.AsUint() << " doesn't exist";
     if (vertex->AddLabel(storage::LabelId::FromUint(label_dist(gen)))
             .HasValue()) {
-      acc.Commit();
+      CHECK(acc.Commit() == std::nullopt);
     } else {
       acc.Abort();
     }
@@ -61,7 +61,7 @@ int main(int argc, char *argv[]) {
       for (int i = 0; i < FLAGS_num_vertices; ++i) {
         vertices.push_back(acc.CreateVertex().Gid());
       }
-      acc.Commit();
+      CHECK(acc.Commit() == std::nullopt);
     }
 
     utils::Timer timer;
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index 804e26c19..c589e8788 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -310,6 +310,9 @@ target_link_libraries(${test_prefix}auth mg-auth kvstore_lib)
 add_unit_test(property_value_v2.cpp)
 target_link_libraries(${test_prefix}property_value_v2 mg-utils)
 
+add_unit_test(storage_v2_constraints.cpp)
+target_link_libraries(${test_prefix}storage_v2_constraints mg-storage-v2)
+
 add_unit_test(storage_v2_edge.cpp)
 target_link_libraries(${test_prefix}storage_v2_edge mg-storage-v2)
 
diff --git a/tests/unit/storage_v2.cpp b/tests/unit/storage_v2.cpp
index be474f03b..d80a01223 100644
--- a/tests/unit/storage_v2.cpp
+++ b/tests/unit/storage_v2.cpp
@@ -25,7 +25,7 @@ TEST(StorageV2, Commit) {
     EXPECT_EQ(CountVertices(&acc, storage::View::OLD), 0U);
     ASSERT_TRUE(acc.FindVertex(gid, storage::View::NEW).has_value());
     EXPECT_EQ(CountVertices(&acc, storage::View::NEW), 1U);
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
   {
     auto acc = store.Access();
@@ -45,7 +45,7 @@ TEST(StorageV2, Commit) {
     EXPECT_EQ(CountVertices(&acc, storage::View::OLD), 1U);
     EXPECT_EQ(CountVertices(&acc, storage::View::NEW), 0U);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
   {
     auto acc = store.Access();
@@ -111,7 +111,7 @@ TEST(StorageV2, AdvanceCommandCommit) {
     ASSERT_TRUE(acc.FindVertex(gid1, storage::View::OLD).has_value());
     ASSERT_TRUE(acc.FindVertex(gid1, storage::View::NEW).has_value());
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
   {
     auto acc = store.Access();
@@ -185,7 +185,7 @@ TEST(StorageV2, SnapshotIsolation) {
   EXPECT_EQ(CountVertices(&acc1, storage::View::NEW), 1U);
   EXPECT_EQ(CountVertices(&acc2, storage::View::NEW), 0U);
 
-  acc1.Commit();
+  ASSERT_EQ(acc1.Commit(), std::nullopt);
 
   ASSERT_FALSE(acc2.FindVertex(gid, storage::View::OLD).has_value());
   EXPECT_EQ(CountVertices(&acc2, storage::View::OLD), 0U);
@@ -224,7 +224,7 @@ TEST(StorageV2, AccessorMove) {
     ASSERT_TRUE(moved.FindVertex(gid, storage::View::NEW).has_value());
     EXPECT_EQ(CountVertices(&moved, storage::View::NEW), 1U);
 
-    moved.Commit();
+    ASSERT_EQ(moved.Commit(), std::nullopt);
   }
   {
     auto acc = store.Access();
@@ -253,7 +253,7 @@ TEST(StorageV2, VertexDeleteCommit) {
     EXPECT_EQ(CountVertices(&acc2, storage::View::OLD), 0U);
     ASSERT_TRUE(acc2.FindVertex(gid, storage::View::NEW).has_value());
     EXPECT_EQ(CountVertices(&acc2, storage::View::NEW), 1U);
-    acc2.Commit();
+    ASSERT_EQ(acc2.Commit(), std::nullopt);
   }
 
   auto acc3 = store.Access();  // read transaction
@@ -283,7 +283,7 @@ TEST(StorageV2, VertexDeleteCommit) {
     EXPECT_EQ(CountVertices(&acc4, storage::View::OLD), 1U);
     EXPECT_EQ(CountVertices(&acc4, storage::View::NEW), 0U);
 
-    acc4.Commit();
+    ASSERT_EQ(acc4.Commit(), std::nullopt);
   }
 
   auto acc5 = store.Access();  // read transaction
@@ -324,7 +324,7 @@ TEST(StorageV2, VertexDeleteAbort) {
     EXPECT_EQ(CountVertices(&acc2, storage::View::OLD), 0U);
     ASSERT_TRUE(acc2.FindVertex(gid, storage::View::NEW).has_value());
     EXPECT_EQ(CountVertices(&acc2, storage::View::NEW), 1U);
-    acc2.Commit();
+    ASSERT_EQ(acc2.Commit(), std::nullopt);
   }
 
   auto acc3 = store.Access();  // read transaction
@@ -390,7 +390,7 @@ TEST(StorageV2, VertexDeleteAbort) {
     EXPECT_EQ(CountVertices(&acc6, storage::View::OLD), 1U);
     EXPECT_EQ(CountVertices(&acc6, storage::View::NEW), 0U);
 
-    acc6.Commit();
+    ASSERT_EQ(acc6.Commit(), std::nullopt);
   }
 
   auto acc7 = store.Access();  // read transaction
@@ -420,10 +420,10 @@ TEST(StorageV2, VertexDeleteAbort) {
   EXPECT_EQ(CountVertices(&acc7, storage::View::NEW), 0U);
 
   // Commit all accessors
-  acc1.Commit();
-  acc3.Commit();
-  acc5.Commit();
-  acc7.Commit();
+  ASSERT_EQ(acc1.Commit(), std::nullopt);
+  ASSERT_EQ(acc3.Commit(), std::nullopt);
+  ASSERT_EQ(acc5.Commit(), std::nullopt);
+  ASSERT_EQ(acc7.Commit(), std::nullopt);
 }
 
 // NOLINTNEXTLINE(hicpp-special-member-functions)
@@ -437,7 +437,7 @@ TEST(StorageV2, VertexDeleteSerializationError) {
     auto acc = store.Access();
     auto vertex = acc.CreateVertex();
     gid = vertex.Gid();
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   auto acc1 = store.Access();
@@ -481,7 +481,7 @@ TEST(StorageV2, VertexDeleteSerializationError) {
   }
 
   // Finalize both accessors
-  acc1.Commit();
+  ASSERT_EQ(acc1.Commit(), std::nullopt);
   acc2.Abort();
 
   // Check whether the vertex exists
@@ -491,7 +491,7 @@ TEST(StorageV2, VertexDeleteSerializationError) {
     ASSERT_FALSE(vertex);
     EXPECT_EQ(CountVertices(&acc, storage::View::OLD), 0U);
     EXPECT_EQ(CountVertices(&acc, storage::View::NEW), 0U);
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 }
 
@@ -535,7 +535,7 @@ TEST(StorageV2, VertexDeleteSpecialCases) {
     ASSERT_TRUE(res.GetValue());
     EXPECT_EQ(CountVertices(&acc, storage::View::OLD), 0U);
     EXPECT_EQ(CountVertices(&acc, storage::View::NEW), 0U);
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the vertices exist
@@ -564,7 +564,7 @@ TEST(StorageV2, VertexDeleteLabel) {
     gid = vertex.Gid();
     ASSERT_FALSE(acc.FindVertex(gid, storage::View::OLD).has_value());
     ASSERT_TRUE(acc.FindVertex(gid, storage::View::NEW).has_value());
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Add label, delete the vertex and check the label API (same command)
@@ -725,7 +725,7 @@ TEST(StorageV2, VertexDeleteProperty) {
     gid = vertex.Gid();
     ASSERT_FALSE(acc.FindVertex(gid, storage::View::OLD).has_value());
     ASSERT_TRUE(acc.FindVertex(gid, storage::View::NEW).has_value());
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Set property, delete the vertex and check the property API (same command)
@@ -743,9 +743,8 @@ TEST(StorageV2, VertexDeleteProperty) {
     ASSERT_EQ(vertex->Properties(storage::View::NEW)->size(), 0);
 
     // Set property 5 to "nandare"
-    ASSERT_TRUE(
-        vertex->SetProperty(property, storage::PropertyValue("nandare"))
-            .GetValue());
+    ASSERT_TRUE(vertex->SetProperty(property, storage::PropertyValue("nandare"))
+                    .GetValue());
 
     // Check whether property 5 exists
     ASSERT_TRUE(vertex->GetProperty(property, storage::View::OLD)->IsNull());
@@ -796,9 +795,8 @@ TEST(StorageV2, VertexDeleteProperty) {
     ASSERT_EQ(vertex->Properties(storage::View::NEW)->size(), 0);
 
     // Set property 5 to "nandare"
-    ASSERT_TRUE(
-        vertex->SetProperty(property, storage::PropertyValue("nandare"))
-            .GetValue());
+    ASSERT_TRUE(vertex->SetProperty(property, storage::PropertyValue("nandare"))
+                    .GetValue());
 
     // Check whether property 5 exists
     ASSERT_TRUE(vertex->GetProperty(property, storage::View::OLD)->IsNull());
@@ -905,7 +903,7 @@ TEST(StorageV2, VertexLabelCommit) {
       ASSERT_FALSE(res.GetValue());
     }
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
   {
     auto acc = store.Access();
@@ -964,7 +962,7 @@ TEST(StorageV2, VertexLabelCommit) {
       ASSERT_FALSE(res.GetValue());
     }
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
   {
     auto acc = store.Access();
@@ -998,7 +996,7 @@ TEST(StorageV2, VertexLabelAbort) {
     auto acc = store.Access();
     auto vertex = acc.CreateVertex();
     gid = vertex.Gid();
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Add label 5, but abort the transaction.
@@ -1085,7 +1083,7 @@ TEST(StorageV2, VertexLabelAbort) {
       ASSERT_FALSE(res.GetValue());
     }
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check that label 5 exists.
@@ -1211,7 +1209,7 @@ TEST(StorageV2, VertexLabelAbort) {
       ASSERT_FALSE(res.GetValue());
     }
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check that label 5 doesn't exist.
@@ -1245,7 +1243,7 @@ TEST(StorageV2, VertexLabelSerializationError) {
     auto acc = store.Access();
     auto vertex = acc.CreateVertex();
     gid = vertex.Gid();
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   auto acc1 = store.Access();
@@ -1313,7 +1311,7 @@ TEST(StorageV2, VertexLabelSerializationError) {
   }
 
   // Finalize both accessors.
-  acc1.Commit();
+  ASSERT_EQ(acc1.Commit(), std::nullopt);
   acc2.Abort();
 
   // Check which labels exist.
@@ -1390,7 +1388,7 @@ TEST(StorageV2, VertexPropertyCommit) {
       ASSERT_EQ(properties[property].ValueString(), "nandare");
     }
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
   {
     auto acc = store.Access();
@@ -1454,7 +1452,7 @@ TEST(StorageV2, VertexPropertyCommit) {
       ASSERT_TRUE(res.GetValue());
     }
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
   {
     auto acc = store.Access();
@@ -1490,7 +1488,7 @@ TEST(StorageV2, VertexPropertyAbort) {
     auto acc = store.Access();
     auto vertex = acc.CreateVertex();
     gid = vertex.Gid();
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Set property 5 to "nandare", but abort the transaction.
@@ -1601,7 +1599,7 @@ TEST(StorageV2, VertexPropertyAbort) {
       ASSERT_EQ(properties[property].ValueString(), "nandare");
     }
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check that property 5 is "nandare".
@@ -1757,7 +1755,7 @@ TEST(StorageV2, VertexPropertyAbort) {
     ASSERT_TRUE(vertex->GetProperty(property, storage::View::NEW)->IsNull());
     ASSERT_EQ(vertex->Properties(storage::View::NEW)->size(), 0);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check that property 5 is null.
@@ -1793,7 +1791,7 @@ TEST(StorageV2, VertexPropertySerializationError) {
     auto acc = store.Access();
     auto vertex = acc.CreateVertex();
     gid = vertex.Gid();
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   auto acc1 = store.Access();
@@ -1857,7 +1855,7 @@ TEST(StorageV2, VertexPropertySerializationError) {
   }
 
   // Finalize both accessors.
-  acc1.Commit();
+  ASSERT_EQ(acc1.Commit(), std::nullopt);
   acc2.Abort();
 
   // Check which properties exist.
@@ -1942,7 +1940,7 @@ TEST(StorageV2, VertexLabelPropertyMixed) {
 
   // Set property 5 to "nandare"
   ASSERT_TRUE(vertex.SetProperty(property, storage::PropertyValue("nandare"))
-                   .GetValue());
+                  .GetValue());
 
   // Check whether label 5 and property 5 exist
   ASSERT_TRUE(vertex.HasLabel(label, storage::View::OLD).GetValue());
@@ -2000,7 +1998,7 @@ TEST(StorageV2, VertexLabelPropertyMixed) {
 
   // Set property 5 to "haihai"
   ASSERT_FALSE(vertex.SetProperty(property, storage::PropertyValue("haihai"))
-                  .GetValue());
+                   .GetValue());
 
   // Check whether label 5 and property 5 exist
   ASSERT_TRUE(vertex.HasLabel(label, storage::View::OLD).GetValue());
@@ -2143,5 +2141,5 @@ TEST(StorageV2, VertexLabelPropertyMixed) {
   ASSERT_EQ(vertex.Properties(storage::View::OLD)->size(), 0);
   ASSERT_EQ(vertex.Properties(storage::View::NEW)->size(), 0);
 
-  acc.Commit();
+  ASSERT_EQ(acc.Commit(), std::nullopt);
 }
diff --git a/tests/unit/storage_v2_constraints.cpp b/tests/unit/storage_v2_constraints.cpp
new file mode 100644
index 000000000..c3d5861f7
--- /dev/null
+++ b/tests/unit/storage_v2_constraints.cpp
@@ -0,0 +1,166 @@
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "storage/v2/storage.hpp"
+
+// NOLINTNEXTLINE(google-build-using-namespace)
+using namespace storage;
+
+// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
+#define ASSERT_NO_ERROR(result) ASSERT_FALSE((result).HasError())
+
+bool operator==(const ExistenceConstraintViolation &lhs,
+                const ExistenceConstraintViolation &rhs) {
+  return lhs.label == rhs.label && lhs.property == rhs.property;
+}
+
+class ConstraintsTest : public testing::Test {
+ protected:
+  ConstraintsTest()
+      : prop1(storage.NameToProperty("prop1")),
+        prop2(storage.NameToProperty("prop2")),
+        label1(storage.NameToLabel("label1")),
+        label2(storage.NameToLabel("label2")) {}
+
+  Storage storage;
+  PropertyId prop1;
+  PropertyId prop2;
+  LabelId label1;
+  LabelId label2;
+};
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(ConstraintsTest, CreateAndDrop) {
+  {
+    auto res = storage.CreateExistenceConstraint(label1, prop1);
+    EXPECT_TRUE(res.HasValue() && res.GetValue());
+  }
+  {
+    auto res = storage.CreateExistenceConstraint(label1, prop1);
+    EXPECT_TRUE(res.HasValue() && !res.GetValue());
+  }
+  {
+    auto res = storage.CreateExistenceConstraint(label2, prop1);
+    EXPECT_TRUE(res.HasValue() && res.GetValue());
+  }
+  EXPECT_TRUE(storage.DropExistenceConstraint(label1, prop1));
+  EXPECT_FALSE(storage.DropExistenceConstraint(label1, prop1));
+  EXPECT_TRUE(storage.DropExistenceConstraint(label2, prop1));
+  EXPECT_FALSE(storage.DropExistenceConstraint(label2, prop2));
+  {
+    auto res = storage.CreateExistenceConstraint(label2, prop1);
+    EXPECT_TRUE(res.HasValue() && res.GetValue());
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(ConstraintsTest, CreateFailure1) {
+  {
+    auto acc = storage.Access();
+    auto vertex = acc.CreateVertex();
+    ASSERT_NO_ERROR(vertex.AddLabel(label1));
+    ASSERT_EQ(acc.Commit(), std::nullopt);
+  }
+  {
+    auto res = storage.CreateExistenceConstraint(label1, prop1);
+    EXPECT_TRUE(
+        res.HasError() &&
+        (res.GetError() == ExistenceConstraintViolation{label1, prop1}));
+  }
+  {
+    auto acc = storage.Access();
+    for (auto vertex : acc.Vertices(View::OLD)) {
+      ASSERT_NO_ERROR(acc.DeleteVertex(&vertex));
+    }
+    ASSERT_EQ(acc.Commit(), std::nullopt);
+  }
+  {
+    auto res = storage.CreateExistenceConstraint(label1, prop1);
+    EXPECT_TRUE(res.HasValue() && res.GetValue());
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(ConstraintsTest, CreateFailure2) {
+  {
+    auto acc = storage.Access();
+    auto vertex = acc.CreateVertex();
+    ASSERT_NO_ERROR(vertex.AddLabel(label1));
+    ASSERT_EQ(acc.Commit(), std::nullopt);
+  }
+  {
+    auto res = storage.CreateExistenceConstraint(label1, prop1);
+    EXPECT_TRUE(
+        res.HasError() &&
+        (res.GetError() == ExistenceConstraintViolation{label1, prop1}));
+  }
+  {
+    auto acc = storage.Access();
+    for (auto vertex : acc.Vertices(View::OLD)) {
+      ASSERT_NO_ERROR(vertex.SetProperty(prop1, PropertyValue(1)));
+    }
+    ASSERT_EQ(acc.Commit(), std::nullopt);
+  }
+  {
+    auto res = storage.CreateExistenceConstraint(label1, prop1);
+    EXPECT_TRUE(res.HasValue() && res.GetValue());
+  }
+}
+
+// NOLINTNEXTLINE(hicpp-special-member-functions)
+TEST_F(ConstraintsTest, ViolationOnCommit) {
+  {
+    auto res = storage.CreateExistenceConstraint(label1, prop1);
+    ASSERT_TRUE(res.HasValue() && res.GetValue());
+  }
+
+  {
+    auto acc = storage.Access();
+    auto vertex = acc.CreateVertex();
+    ASSERT_NO_ERROR(vertex.AddLabel(label1));
+
+    auto res = acc.Commit();
+    EXPECT_TRUE(res.has_value() &&
+                (*res == ExistenceConstraintViolation{label1, prop1}));
+  }
+
+  {
+    auto acc = storage.Access();
+    auto vertex = acc.CreateVertex();
+    ASSERT_NO_ERROR(vertex.AddLabel(label1));
+    ASSERT_NO_ERROR(vertex.SetProperty(prop1, PropertyValue(1)));
+    EXPECT_EQ(acc.Commit(), std::nullopt);
+  }
+
+  {
+    auto acc = storage.Access();
+    for (auto vertex : acc.Vertices(View::OLD)) {
+      ASSERT_NO_ERROR(vertex.SetProperty(prop1, PropertyValue()));
+    }
+
+    auto res = acc.Commit();
+    EXPECT_TRUE(res.has_value() &&
+                (*res == ExistenceConstraintViolation{label1, prop1}));
+  }
+
+  {
+    auto acc = storage.Access();
+    for (auto vertex : acc.Vertices(View::OLD)) {
+      ASSERT_NO_ERROR(vertex.SetProperty(prop1, PropertyValue()));
+    }
+    for (auto vertex : acc.Vertices(View::OLD)) {
+      ASSERT_NO_ERROR(acc.DeleteVertex(&vertex));
+    }
+
+    EXPECT_EQ(acc.Commit(), std::nullopt);
+  }
+
+  ASSERT_TRUE(storage.DropExistenceConstraint(label1, prop1));
+
+  {
+    auto acc = storage.Access();
+    auto vertex = acc.CreateVertex();
+    ASSERT_NO_ERROR(vertex.AddLabel(label1));
+    EXPECT_EQ(acc.Commit(), std::nullopt);
+  }
+}
diff --git a/tests/unit/storage_v2_edge.cpp b/tests/unit/storage_v2_edge.cpp
index a77c551e8..21d3f9f64 100644
--- a/tests/unit/storage_v2_edge.cpp
+++ b/tests/unit/storage_v2_edge.cpp
@@ -19,7 +19,7 @@ TEST(StorageV2, EdgeCreateFromSmallerCommit) {
     auto vertex_to = acc.CreateVertex();
     gid_from = vertex_from.Gid();
     gid_to = vertex_to.Gid();
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Create edge
@@ -77,7 +77,7 @@ TEST(StorageV2, EdgeCreateFromSmallerCommit) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::NEW)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the edge exists
@@ -152,7 +152,7 @@ TEST(StorageV2, EdgeCreateFromSmallerCommit) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::NEW)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 }
 
@@ -171,7 +171,7 @@ TEST(StorageV2, EdgeCreateFromLargerCommit) {
     auto vertex_from = acc.CreateVertex();
     gid_to = vertex_to.Gid();
     gid_from = vertex_from.Gid();
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Create edge
@@ -229,7 +229,7 @@ TEST(StorageV2, EdgeCreateFromLargerCommit) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::NEW)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the edge exists
@@ -304,7 +304,7 @@ TEST(StorageV2, EdgeCreateFromLargerCommit) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::NEW)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 }
 
@@ -319,7 +319,7 @@ TEST(StorageV2, EdgeCreateFromSameCommit) {
     auto acc = store.Access();
     auto vertex = acc.CreateVertex();
     gid_vertex = vertex.Gid();
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Create edge
@@ -369,7 +369,7 @@ TEST(StorageV2, EdgeCreateFromSameCommit) {
     ASSERT_EQ(vertex->InEdges({other_et}, storage::View::NEW)->size(), 0);
     ASSERT_EQ(vertex->InEdges({et, other_et}, storage::View::NEW)->size(), 1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the edge exists
@@ -434,7 +434,7 @@ TEST(StorageV2, EdgeCreateFromSameCommit) {
     ASSERT_EQ(vertex->OutEdges({et, other_et}, storage::View::OLD)->size(), 1);
     ASSERT_EQ(vertex->OutEdges({et, other_et}, storage::View::NEW)->size(), 1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 }
 
@@ -453,7 +453,7 @@ TEST(StorageV2, EdgeCreateFromSmallerAbort) {
     auto vertex_to = acc.CreateVertex();
     gid_from = vertex_from.Gid();
     gid_to = vertex_to.Gid();
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Create edge, but abort the transaction
@@ -532,7 +532,7 @@ TEST(StorageV2, EdgeCreateFromSmallerAbort) {
     ASSERT_EQ(vertex_to->OutEdges({}, storage::View::OLD)->size(), 0);
     ASSERT_EQ(vertex_to->OutEdges({}, storage::View::NEW)->size(), 0);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Create edge
@@ -590,7 +590,7 @@ TEST(StorageV2, EdgeCreateFromSmallerAbort) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::NEW)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the edge exists
@@ -665,7 +665,7 @@ TEST(StorageV2, EdgeCreateFromSmallerAbort) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::NEW)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 }
 
@@ -684,7 +684,7 @@ TEST(StorageV2, EdgeCreateFromLargerAbort) {
     auto vertex_from = acc.CreateVertex();
     gid_to = vertex_to.Gid();
     gid_from = vertex_from.Gid();
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Create edge, but abort the transaction
@@ -763,7 +763,7 @@ TEST(StorageV2, EdgeCreateFromLargerAbort) {
     ASSERT_EQ(vertex_to->OutEdges({}, storage::View::OLD)->size(), 0);
     ASSERT_EQ(vertex_to->OutEdges({}, storage::View::NEW)->size(), 0);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Create edge
@@ -821,7 +821,7 @@ TEST(StorageV2, EdgeCreateFromLargerAbort) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::NEW)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the edge exists
@@ -896,7 +896,7 @@ TEST(StorageV2, EdgeCreateFromLargerAbort) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::NEW)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 }
 
@@ -911,7 +911,7 @@ TEST(StorageV2, EdgeCreateFromSameAbort) {
     auto acc = store.Access();
     auto vertex = acc.CreateVertex();
     gid_vertex = vertex.Gid();
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Create edge, but abort the transaction
@@ -976,7 +976,7 @@ TEST(StorageV2, EdgeCreateFromSameAbort) {
     ASSERT_EQ(vertex->OutEdges({}, storage::View::OLD)->size(), 0);
     ASSERT_EQ(vertex->OutEdges({}, storage::View::NEW)->size(), 0);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Create edge
@@ -1026,7 +1026,7 @@ TEST(StorageV2, EdgeCreateFromSameAbort) {
     ASSERT_EQ(vertex->InEdges({other_et}, storage::View::NEW)->size(), 0);
     ASSERT_EQ(vertex->InEdges({et, other_et}, storage::View::NEW)->size(), 1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the edge exists
@@ -1091,7 +1091,7 @@ TEST(StorageV2, EdgeCreateFromSameAbort) {
     ASSERT_EQ(vertex->OutEdges({et, other_et}, storage::View::OLD)->size(), 1);
     ASSERT_EQ(vertex->OutEdges({et, other_et}, storage::View::NEW)->size(), 1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 }
 
@@ -1110,7 +1110,7 @@ TEST(StorageV2, EdgeDeleteFromSmallerCommit) {
     auto vertex_to = acc.CreateVertex();
     gid_from = vertex_from.Gid();
     gid_to = vertex_to.Gid();
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Create edge
@@ -1168,7 +1168,7 @@ TEST(StorageV2, EdgeDeleteFromSmallerCommit) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::NEW)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the edge exists
@@ -1243,7 +1243,7 @@ TEST(StorageV2, EdgeDeleteFromSmallerCommit) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::NEW)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Delete edge
@@ -1300,7 +1300,7 @@ TEST(StorageV2, EdgeDeleteFromSmallerCommit) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::OLD)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the edge exists
@@ -1321,7 +1321,7 @@ TEST(StorageV2, EdgeDeleteFromSmallerCommit) {
     ASSERT_EQ(vertex_to->OutEdges({}, storage::View::OLD)->size(), 0);
     ASSERT_EQ(vertex_to->OutEdges({}, storage::View::NEW)->size(), 0);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 }
 
@@ -1340,7 +1340,7 @@ TEST(StorageV2, EdgeDeleteFromLargerCommit) {
     auto vertex_from = acc.CreateVertex();
     gid_from = vertex_from.Gid();
     gid_to = vertex_to.Gid();
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Create edge
@@ -1398,7 +1398,7 @@ TEST(StorageV2, EdgeDeleteFromLargerCommit) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::NEW)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the edge exists
@@ -1473,7 +1473,7 @@ TEST(StorageV2, EdgeDeleteFromLargerCommit) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::NEW)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Delete edge
@@ -1530,7 +1530,7 @@ TEST(StorageV2, EdgeDeleteFromLargerCommit) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::OLD)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the edge exists
@@ -1551,7 +1551,7 @@ TEST(StorageV2, EdgeDeleteFromLargerCommit) {
     ASSERT_EQ(vertex_to->OutEdges({}, storage::View::OLD)->size(), 0);
     ASSERT_EQ(vertex_to->OutEdges({}, storage::View::NEW)->size(), 0);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 }
 
@@ -1566,7 +1566,7 @@ TEST(StorageV2, EdgeDeleteFromSameCommit) {
     auto acc = store.Access();
     auto vertex = acc.CreateVertex();
     gid_vertex = vertex.Gid();
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Create edge
@@ -1616,7 +1616,7 @@ TEST(StorageV2, EdgeDeleteFromSameCommit) {
     ASSERT_EQ(vertex->InEdges({other_et}, storage::View::NEW)->size(), 0);
     ASSERT_EQ(vertex->InEdges({et, other_et}, storage::View::NEW)->size(), 1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the edge exists
@@ -1681,7 +1681,7 @@ TEST(StorageV2, EdgeDeleteFromSameCommit) {
     ASSERT_EQ(vertex->OutEdges({et, other_et}, storage::View::OLD)->size(), 1);
     ASSERT_EQ(vertex->OutEdges({et, other_et}, storage::View::NEW)->size(), 1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Delete edge
@@ -1730,7 +1730,7 @@ TEST(StorageV2, EdgeDeleteFromSameCommit) {
     ASSERT_EQ(vertex->OutEdges({other_et}, storage::View::OLD)->size(), 0);
     ASSERT_EQ(vertex->OutEdges({et, other_et}, storage::View::OLD)->size(), 1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the edge exists
@@ -1745,7 +1745,7 @@ TEST(StorageV2, EdgeDeleteFromSameCommit) {
     ASSERT_EQ(vertex->OutEdges({}, storage::View::OLD)->size(), 0);
     ASSERT_EQ(vertex->OutEdges({}, storage::View::NEW)->size(), 0);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 }
 
@@ -1764,7 +1764,7 @@ TEST(StorageV2, EdgeDeleteFromSmallerAbort) {
     auto vertex_to = acc.CreateVertex();
     gid_from = vertex_from.Gid();
     gid_to = vertex_to.Gid();
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Create edge
@@ -1822,7 +1822,7 @@ TEST(StorageV2, EdgeDeleteFromSmallerAbort) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::NEW)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the edge exists
@@ -1897,7 +1897,7 @@ TEST(StorageV2, EdgeDeleteFromSmallerAbort) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::NEW)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Delete the edge, but abort the transaction
@@ -2029,7 +2029,7 @@ TEST(StorageV2, EdgeDeleteFromSmallerAbort) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::NEW)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Delete the edge
@@ -2086,7 +2086,7 @@ TEST(StorageV2, EdgeDeleteFromSmallerAbort) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::OLD)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the edge exists
@@ -2107,7 +2107,7 @@ TEST(StorageV2, EdgeDeleteFromSmallerAbort) {
     ASSERT_EQ(vertex_to->OutEdges({}, storage::View::OLD)->size(), 0);
     ASSERT_EQ(vertex_to->OutEdges({}, storage::View::NEW)->size(), 0);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 }
 
@@ -2126,7 +2126,7 @@ TEST(StorageV2, EdgeDeleteFromLargerAbort) {
     auto vertex_to = acc.CreateVertex();
     gid_from = vertex_from.Gid();
     gid_to = vertex_to.Gid();
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Create edge
@@ -2184,7 +2184,7 @@ TEST(StorageV2, EdgeDeleteFromLargerAbort) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::NEW)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the edge exists
@@ -2259,7 +2259,7 @@ TEST(StorageV2, EdgeDeleteFromLargerAbort) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::NEW)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Delete the edge, but abort the transaction
@@ -2391,7 +2391,7 @@ TEST(StorageV2, EdgeDeleteFromLargerAbort) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::NEW)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Delete the edge
@@ -2448,7 +2448,7 @@ TEST(StorageV2, EdgeDeleteFromLargerAbort) {
     ASSERT_EQ(vertex_to->InEdges({et, other_et}, storage::View::OLD)->size(),
               1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the edge exists
@@ -2469,7 +2469,7 @@ TEST(StorageV2, EdgeDeleteFromLargerAbort) {
     ASSERT_EQ(vertex_to->OutEdges({}, storage::View::OLD)->size(), 0);
     ASSERT_EQ(vertex_to->OutEdges({}, storage::View::NEW)->size(), 0);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 }
 
@@ -2484,7 +2484,7 @@ TEST(StorageV2, EdgeDeleteFromSameAbort) {
     auto acc = store.Access();
     auto vertex = acc.CreateVertex();
     gid_vertex = vertex.Gid();
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Create edge
@@ -2534,7 +2534,7 @@ TEST(StorageV2, EdgeDeleteFromSameAbort) {
     ASSERT_EQ(vertex->InEdges({other_et}, storage::View::NEW)->size(), 0);
     ASSERT_EQ(vertex->InEdges({et, other_et}, storage::View::NEW)->size(), 1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the edge exists
@@ -2599,7 +2599,7 @@ TEST(StorageV2, EdgeDeleteFromSameAbort) {
     ASSERT_EQ(vertex->OutEdges({et, other_et}, storage::View::OLD)->size(), 1);
     ASSERT_EQ(vertex->OutEdges({et, other_et}, storage::View::NEW)->size(), 1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Delete the edge, but abort the transaction
@@ -2713,7 +2713,7 @@ TEST(StorageV2, EdgeDeleteFromSameAbort) {
     ASSERT_EQ(vertex->OutEdges({et, other_et}, storage::View::OLD)->size(), 1);
     ASSERT_EQ(vertex->OutEdges({et, other_et}, storage::View::NEW)->size(), 1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Delete the edge
@@ -2762,7 +2762,7 @@ TEST(StorageV2, EdgeDeleteFromSameAbort) {
     ASSERT_EQ(vertex->OutEdges({other_et}, storage::View::OLD)->size(), 0);
     ASSERT_EQ(vertex->OutEdges({et, other_et}, storage::View::OLD)->size(), 1);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check whether the edge exists
@@ -2777,7 +2777,7 @@ TEST(StorageV2, EdgeDeleteFromSameAbort) {
     ASSERT_EQ(vertex->OutEdges({}, storage::View::OLD)->size(), 0);
     ASSERT_EQ(vertex->OutEdges({}, storage::View::NEW)->size(), 0);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 }
 
@@ -2831,7 +2831,7 @@ TEST(StorageV2, VertexDetachDeleteSingleCommit) {
     }
     ASSERT_EQ(vertex_to.OutEdges({}, storage::View::NEW)->size(), 0);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Detach delete vertex
@@ -2888,7 +2888,7 @@ TEST(StorageV2, VertexDetachDeleteSingleCommit) {
     ASSERT_EQ(vertex_to->OutEdges({}, storage::View::OLD)->size(), 0);
     ASSERT_EQ(vertex_to->OutEdges({}, storage::View::NEW)->size(), 0);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check dataset
@@ -3043,7 +3043,7 @@ TEST(StorageV2, VertexDetachDeleteMultipleCommit) {
       }
     }
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Detach delete vertex
@@ -3183,7 +3183,7 @@ TEST(StorageV2, VertexDetachDeleteMultipleCommit) {
       ASSERT_EQ(e.ToVertex(), *vertex2);
     }
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check dataset
@@ -3290,7 +3290,7 @@ TEST(StorageV2, VertexDetachDeleteSingleAbort) {
     }
     ASSERT_EQ(vertex_to.OutEdges({}, storage::View::NEW)->size(), 0);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Detach delete vertex, but abort the transaction
@@ -3384,7 +3384,7 @@ TEST(StorageV2, VertexDetachDeleteSingleAbort) {
     }
     ASSERT_EQ(vertex_to->OutEdges({}, storage::View::NEW)->size(), 0);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Detach delete vertex
@@ -3441,7 +3441,7 @@ TEST(StorageV2, VertexDetachDeleteSingleAbort) {
     ASSERT_EQ(vertex_to->OutEdges({}, storage::View::OLD)->size(), 0);
     ASSERT_EQ(vertex_to->OutEdges({}, storage::View::NEW)->size(), 0);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check dataset
@@ -3596,7 +3596,7 @@ TEST(StorageV2, VertexDetachDeleteMultipleAbort) {
       }
     }
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Detach delete vertex, but abort the transaction
@@ -3922,7 +3922,7 @@ TEST(StorageV2, VertexDetachDeleteMultipleAbort) {
       }
     }
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Detach delete vertex
@@ -4062,7 +4062,7 @@ TEST(StorageV2, VertexDetachDeleteMultipleAbort) {
       ASSERT_EQ(e.ToVertex(), *vertex2);
     }
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check dataset
@@ -4168,7 +4168,7 @@ TEST(StorageV2, EdgePropertyCommit) {
       ASSERT_EQ(properties[property].ValueString(), "nandare");
     }
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
   {
     auto acc = store.Access();
@@ -4232,7 +4232,7 @@ TEST(StorageV2, EdgePropertyCommit) {
       ASSERT_TRUE(res.GetValue());
     }
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
   {
     auto acc = store.Access();
@@ -4272,7 +4272,7 @@ TEST(StorageV2, EdgePropertyAbort) {
     ASSERT_EQ(edge.EdgeType(), et);
     ASSERT_EQ(edge.FromVertex(), vertex);
     ASSERT_EQ(edge.ToVertex(), vertex);
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Set property 5 to "nandare", but abort the transaction.
@@ -4382,7 +4382,7 @@ TEST(StorageV2, EdgePropertyAbort) {
       ASSERT_EQ(properties[property].ValueString(), "nandare");
     }
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check that property 5 is "nandare".
@@ -4538,7 +4538,7 @@ TEST(StorageV2, EdgePropertyAbort) {
     ASSERT_TRUE(edge.GetProperty(property, storage::View::NEW)->IsNull());
     ASSERT_EQ(edge.Properties(storage::View::NEW)->size(), 0);
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Check that property 5 is null.
@@ -4578,7 +4578,7 @@ TEST(StorageV2, EdgePropertySerializationError) {
     ASSERT_EQ(edge.EdgeType(), et);
     ASSERT_EQ(edge.FromVertex(), vertex);
     ASSERT_EQ(edge.ToVertex(), vertex);
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   auto acc1 = store.Access();
@@ -4642,7 +4642,7 @@ TEST(StorageV2, EdgePropertySerializationError) {
   }
 
   // Finalize both accessors.
-  acc1.Commit();
+  ASSERT_EQ(acc1.Commit(), std::nullopt);
   acc2.Abort();
 
   // Check which properties exist.
diff --git a/tests/unit/storage_v2_gc.cpp b/tests/unit/storage_v2_gc.cpp
index fed5abe3e..b5238b027 100644
--- a/tests/unit/storage_v2_gc.cpp
+++ b/tests/unit/storage_v2_gc.cpp
@@ -47,7 +47,7 @@ TEST(StorageV2Gc, Sanity) {
       EXPECT_EQ(vertex_new.has_value(), i % 5 != 0);
     }
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Verify existing vertices and add labels to some of them.
@@ -90,7 +90,7 @@ TEST(StorageV2Gc, Sanity) {
       }
     }
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 
   // Add and remove some edges.
@@ -148,7 +148,7 @@ TEST(StorageV2Gc, Sanity) {
       }
     }
 
-    acc.Commit();
+    ASSERT_EQ(acc.Commit(), std::nullopt);
   }
 }
 
@@ -171,7 +171,7 @@ TEST(StorageV2Gc, Indices) {
       auto vertex = acc0.CreateVertex();
       ASSERT_TRUE(*vertex.AddLabel(acc0.NameToLabel("label")));
     }
-    acc0.Commit();
+    ASSERT_EQ(acc0.Commit(), std::nullopt);
   }
   {
     auto acc1 = storage.Access();
@@ -180,7 +180,7 @@ TEST(StorageV2Gc, Indices) {
     for (auto vertex : acc2.Vertices(storage::View::OLD)) {
       ASSERT_TRUE(*vertex.RemoveLabel(acc2.NameToLabel("label")));
     }
-    acc2.Commit();
+    ASSERT_EQ(acc2.Commit(), std::nullopt);
 
     // Wait for GC.
     std::this_thread::sleep_for(std::chrono::milliseconds(300));