diff --git a/src/connectivity/bluetooth/core/bt-host/att/att.h b/src/connectivity/bluetooth/core/bt-host/att/att.h
index 46cd5e5d233c7da290304bec4f97314149a0933d..37d28f1473017c261afa8b9fffd5457a5b89d4b4 100644
--- a/src/connectivity/bluetooth/core/bt-host/att/att.h
+++ b/src/connectivity/bluetooth/core/bt-host/att/att.h
@@ -61,6 +61,10 @@ constexpr OpCode kCommandFlag = (1 << 6);
 // The length of an authentication signature used in a signed PDU.
 constexpr size_t kAuthenticationSignatureLength = 12;
 
+// The maximum number of write requests that can be queued for submission in a
+// ATT Prepare Write Request.
+constexpr uint8_t kPrepareQueueMaxCapacity = 20;
+
 enum class MethodType {
   kInvalid,
   kRequest,
diff --git a/src/connectivity/bluetooth/core/bt-host/gatt/server.cc b/src/connectivity/bluetooth/core/bt-host/gatt/server.cc
index 4479db433483b33c115c253bb2ec1e8e1ce2f6ad..aa58abc0af251f96665275bd969e66dade7cb675 100644
--- a/src/connectivity/bluetooth/core/bt-host/gatt/server.cc
+++ b/src/connectivity/bluetooth/core/bt-host/gatt/server.cc
@@ -18,6 +18,8 @@
 namespace bt {
 namespace gatt {
 
+using common::DynamicByteBuffer;
+
 Server::Server(DeviceId peer_id, fxl::RefPtr<att::Database> database,
                fxl::RefPtr<att::Bearer> bearer)
     : peer_id_(peer_id), db_(database), att_(bearer), weak_ptr_factory_(this) {
@@ -43,12 +45,21 @@ Server::Server(DeviceId peer_id, fxl::RefPtr<att::Database> database,
   read_blob_req_id_ =
       att_->RegisterHandler(att::kReadBlobRequest,
                             fit::bind_member(this, &Server::OnReadBlobRequest));
-  find_by_type_value_ = att_->RegisterHandler(
+  find_by_type_value_id_ = att_->RegisterHandler(
       att::kFindByTypeValueRequest,
       fit::bind_member(this, &Server::OnFindByTypeValueRequest));
+  prepare_write_id_ = att_->RegisterHandler(
+      att::kPrepareWriteRequest,
+      fit::bind_member(this, &Server::OnPrepareWriteRequest));
+  exec_write_id_ = att_->RegisterHandler(
+      att::kExecuteWriteRequest,
+      fit::bind_member(this, &Server::OnExecuteWriteRequest));
 }
 
 Server::~Server() {
+  att_->UnregisterHandler(exec_write_id_);
+  att_->UnregisterHandler(prepare_write_id_);
+  att_->UnregisterHandler(find_by_type_value_id_);
   att_->UnregisterHandler(read_blob_req_id_);
   att_->UnregisterHandler(write_cmd_id_);
   att_->UnregisterHandler(write_req_id_);
@@ -752,5 +763,93 @@ att::ErrorCode Server::ReadByTypeHelper(
   return att::ErrorCode::kNoError;
 }
 
+void Server::OnPrepareWriteRequest(att::Bearer::TransactionId tid,
+                                   const att::PacketReader& packet) {
+  ZX_DEBUG_ASSERT(packet.opcode() == att::kPrepareWriteRequest);
+
+  if (packet.payload_size() < sizeof(att::PrepareWriteRequestParams)) {
+    att_->ReplyWithError(tid, att::kInvalidHandle, att::ErrorCode::kInvalidPDU);
+    return;
+  }
+
+  const auto& params = packet.payload<att::PrepareWriteRequestParams>();
+  att::Handle handle = le16toh(params.handle);
+  uint16_t offset = le16toh(params.offset);
+  auto value_view =
+      packet.payload_data().view(sizeof(params.handle) + sizeof(params.offset));
+
+  if (prepare_queue_.size() >= att::kPrepareQueueMaxCapacity) {
+    att_->ReplyWithError(tid, handle, att::ErrorCode::kPrepareQueueFull);
+    return;
+  }
+
+  // Validate attribute handle and perform security checks (see Vol 3, Part F,
+  // 3.4.6.1 for required checks)
+  const auto* attr = db_->FindAttribute(handle);
+  if (!attr) {
+    att_->ReplyWithError(tid, handle, att::ErrorCode::kInvalidHandle);
+    return;
+  }
+
+  att::ErrorCode ecode =
+      att::CheckWritePermissions(attr->write_reqs(), att_->security());
+  if (ecode != att::ErrorCode::kNoError) {
+    att_->ReplyWithError(tid, handle, ecode);
+    return;
+  }
+
+  prepare_queue_.push(att::QueuedWrite(handle, offset, value_view));
+
+  // Reply back with the request payload.
+  auto buffer = std::make_unique<DynamicByteBuffer>(packet.size());
+  att::PacketWriter writer(att::kPrepareWriteResponse, buffer.get());
+  writer.mutable_payload_data().Write(packet.payload_data());
+
+  att_->Reply(tid, std::move(buffer));
+}
+
+void Server::OnExecuteWriteRequest(att::Bearer::TransactionId tid,
+                                   const att::PacketReader& packet) {
+  ZX_DEBUG_ASSERT(packet.opcode() == att::kExecuteWriteRequest);
+
+  if (packet.payload_size() != sizeof(att::ExecuteWriteRequestParams)) {
+    att_->ReplyWithError(tid, att::kInvalidHandle, att::ErrorCode::kInvalidPDU);
+    return;
+  }
+
+  const auto& params = packet.payload<att::ExecuteWriteRequestParams>();
+  if (params.flags == att::ExecuteWriteFlag::kCancelAll) {
+    prepare_queue_ = {};
+
+    auto buffer = std::make_unique<DynamicByteBuffer>(1);
+    att::PacketWriter writer(att::kExecuteWriteResponse, buffer.get());
+    att_->Reply(tid, std::move(buffer));
+    return;
+  }
+
+  if (params.flags != att::ExecuteWriteFlag::kWritePending) {
+    att_->ReplyWithError(tid, att::kInvalidHandle, att::ErrorCode::kInvalidPDU);
+    return;
+  }
+
+  auto self = weak_ptr_factory_.GetWeakPtr();
+  auto result_cb = [self, tid](att::Handle handle,
+                               att::ErrorCode ecode) mutable {
+    if (!self)
+      return;
+
+    if (ecode != att::ErrorCode::kNoError) {
+      self->att_->ReplyWithError(tid, handle, ecode);
+      return;
+    }
+
+    auto rsp = std::make_unique<DynamicByteBuffer>(1);
+    att::PacketWriter writer(att::kExecuteWriteResponse, rsp.get());
+    self->att_->Reply(tid, std::move(rsp));
+  };
+  db_->ExecuteWriteQueue(peer_id_, std::move(prepare_queue_), att_->security(),
+                         std::move(result_cb));
+}
+
 }  // namespace gatt
 }  // namespace bt
diff --git a/src/connectivity/bluetooth/core/bt-host/gatt/server.h b/src/connectivity/bluetooth/core/bt-host/gatt/server.h
index 20bc42a521a375fe9749c189fec75f7c222f5c60..2f69d76e0395ee0da14df7d1ff8565ff3f668c82 100644
--- a/src/connectivity/bluetooth/core/bt-host/gatt/server.h
+++ b/src/connectivity/bluetooth/core/bt-host/gatt/server.h
@@ -6,6 +6,7 @@
 #define SRC_CONNECTIVITY_BLUETOOTH_CORE_BT_HOST_GATT_SERVER_H_
 
 #include "src/connectivity/bluetooth/core/bt-host/att/bearer.h"
+#include "src/connectivity/bluetooth/core/bt-host/att/database.h"
 #include "src/connectivity/bluetooth/core/bt-host/common/uuid.h"
 #include "src/connectivity/bluetooth/core/bt-host/gatt/gatt_defs.h"
 #include "src/lib/fxl/memory/ref_ptr.h"
@@ -63,6 +64,11 @@ class Server final {
                          const att::PacketReader& packet);
   void OnFindByTypeValueRequest(att::Bearer::TransactionId tid,
                                 const att::PacketReader& packet);
+  void OnPrepareWriteRequest(att::Bearer::TransactionId tid,
+                             const att::PacketReader& packet);
+  void OnExecuteWriteRequest(att::Bearer::TransactionId tid,
+                             const att::PacketReader& packet);
+
   // Helper function to serve the Read By Type and Read By Group Type requests.
   // This searches |db| for attributes with the given |type| and adds as many
   // attributes as it can fit within the given |max_data_list_size|. The
@@ -84,7 +90,13 @@ class Server final {
   fxl::RefPtr<att::Database> db_;
   fxl::RefPtr<att::Bearer> att_;
 
+  // The queue data structure used for queued writes (see Vol 3, Part F, 3.4.6).
+  att::PrepareWriteQueue prepare_queue_;
+
   // ATT protocol request handler IDs
+  // TODO(armansito): Storing all these IDs here feels wasteful. Provide a way
+  // to unregister GATT server callbacks collectively from an att::Bearer, given
+  // that it's server-role functionalities are uniquely handled by this class.
   att::Bearer::HandlerId exchange_mtu_id_;
   att::Bearer::HandlerId find_information_id_;
   att::Bearer::HandlerId read_by_group_type_id_;
@@ -93,7 +105,9 @@ class Server final {
   att::Bearer::HandlerId write_req_id_;
   att::Bearer::HandlerId write_cmd_id_;
   att::Bearer::HandlerId read_blob_req_id_;
-  att::Bearer::HandlerId find_by_type_value_;
+  att::Bearer::HandlerId find_by_type_value_id_;
+  att::Bearer::HandlerId prepare_write_id_;
+  att::Bearer::HandlerId exec_write_id_;
 
   fxl::WeakPtrFactory<Server> weak_ptr_factory_;
 
diff --git a/src/connectivity/bluetooth/core/bt-host/gatt/server_unittest.cc b/src/connectivity/bluetooth/core/bt-host/gatt/server_unittest.cc
index 142319b2343c70d257a576153763022d03be6023..30f5e50a35841d86037b99622bc86153a7de5b61 100644
--- a/src/connectivity/bluetooth/core/bt-host/gatt/server_unittest.cc
+++ b/src/connectivity/bluetooth/core/bt-host/gatt/server_unittest.cc
@@ -1903,6 +1903,421 @@ TEST_F(GATT_ServerTest, ReadRequestSuccess) {
   EXPECT_TRUE(ReceiveAndExpect(kRequest, kExpected));
 }
 
+TEST_F(GATT_ServerTest, PrepareWriteRequestInvalidPDU) {
+  // Payload is one byte too short.
+  // clang-format off
+  const auto kInvalidPDU = common::CreateStaticByteBuffer(
+      0x16,        // opcode: prepare write request
+      0x01, 0x00,  // handle: 0x0001
+      0x01         // offset (should be 2 bytes).
+  );
+  const auto kExpected = common::CreateStaticByteBuffer(
+      0x01,        // opcode: error response
+      0x16,        // request: prepare write request
+      0x00, 0x00,  // handle: 0
+      0x04         // error: Invalid PDU
+  );
+  // clang-format on
+
+  EXPECT_TRUE(ReceiveAndExpect(kInvalidPDU, kExpected));
+}
+
+TEST_F(GATT_ServerTest, PrepareWriteRequestInvalidHandle) {
+  // clang-format off
+  const auto kRequest = common::CreateStaticByteBuffer(
+      0x16,              // opcode: prepare write request
+      0x01, 0x00,         // handle: 0x0001
+      0x00, 0x00,         // offset: 0
+      't', 'e', 's', 't'  // value: "test"
+  );
+  const auto kResponse = common::CreateStaticByteBuffer(
+      0x01,        // opcode: error response
+      0x16,        // request: prepare write request
+      0x01, 0x00,  // handle: 0x0001
+      0x01         // error: invalid handle
+  );
+  // clang-format on
+
+  EXPECT_TRUE(ReceiveAndExpect(kRequest, kResponse));
+}
+
+TEST_F(GATT_ServerTest, PrepareWriteRequestSucceeds) {
+  const auto kTestValue = common::CreateStaticByteBuffer('f', 'o', 'o');
+  auto* grp = db()->NewGrouping(types::kPrimaryService, 1, kTestValue);
+
+  // No security requirement
+  auto* attr = grp->AddAttribute(kTestType16, att::AccessRequirements(),
+                                 att::AccessRequirements(false, false, false));
+  grp->set_active(true);
+
+  int write_count = 0;
+  attr->set_write_handler([&](DeviceId, att::Handle, uint16_t, const auto&,
+                              const auto&) { write_count++; });
+
+  ASSERT_EQ(0x0002, attr->handle());
+
+  // clang-format off
+  const auto kRequest = common::CreateStaticByteBuffer(
+      0x16,              // opcode: prepare write request
+      0x02, 0x00,         // handle: 0x0002
+      0x00, 0x00,         // offset: 0
+      't', 'e', 's', 't'  // value: "test"
+  );
+  const auto kResponse = common::CreateStaticByteBuffer(
+      0x17,              // opcode: prepare write response
+      0x02, 0x00,         // handle: 0x0002
+      0x00, 0x00,         // offset: 0
+      't', 'e', 's', 't'  // value: "test"
+  );
+  // clang-format on
+
+  EXPECT_TRUE(ReceiveAndExpect(kRequest, kResponse));
+
+  // The attribute should not have been written yet.
+  EXPECT_EQ(0, write_count);
+}
+
+TEST_F(GATT_ServerTest, PrepareWriteRequestPrepareQueueFull) {
+  const auto kTestValue = common::CreateStaticByteBuffer('f', 'o', 'o');
+  auto* grp = db()->NewGrouping(types::kPrimaryService, 1, kTestValue);
+
+  // No security requirement
+  const auto* attr =
+      grp->AddAttribute(kTestType16, att::AccessRequirements(),
+                        att::AccessRequirements(false, false, false));
+  grp->set_active(true);
+
+  ASSERT_EQ(0x0002, attr->handle());
+
+  // clang-format off
+  const auto kRequest = common::CreateStaticByteBuffer(
+      0x16,              // opcode: prepare write request
+      0x02, 0x00,         // handle: 0x0002
+      0x00, 0x00,         // offset: 0
+      't', 'e', 's', 't'  // value: "test"
+  );
+  const auto kSuccessResponse = common::CreateStaticByteBuffer(
+      0x17,              // opcode: prepare write response
+      0x02, 0x00,         // handle: 0x0002
+      0x00, 0x00,         // offset: 0
+      't', 'e', 's', 't'  // value: "test"
+  );
+  const auto kErrorResponse = common::CreateStaticByteBuffer(
+      0x01,        // opcode: error response
+      0x16,        // request: prepare write request
+      0x02, 0x00,  // handle: 0x0002
+      0x09         // error: prepare queue full
+  );
+  // clang-format on
+
+  // Write requests should succeed until capacity is filled.
+  for (unsigned i = 0; i < att::kPrepareQueueMaxCapacity; i++) {
+    ASSERT_TRUE(ReceiveAndExpect(kRequest, kSuccessResponse))
+        << "Unexpected failure at attempt: " << i;
+  }
+
+  // The next request should fail with a capacity error.
+  EXPECT_TRUE(ReceiveAndExpect(kRequest, kErrorResponse));
+}
+
+TEST_F(GATT_ServerTest, ExecuteWriteMalformedPayload) {
+  // Payload is one byte too short.
+  // clang-format off
+  const auto kInvalidPDU = common::CreateStaticByteBuffer(
+      0x18  // opcode: execute write request
+  );
+  const auto kExpected = common::CreateStaticByteBuffer(
+      0x01,        // opcode: error response
+      0x18,        // request: execute write request
+      0x00, 0x00,  // handle: 0
+      0x04         // error: Invalid PDU
+  );
+  // clang-format on
+
+  EXPECT_TRUE(ReceiveAndExpect(kInvalidPDU, kExpected));
+}
+
+TEST_F(GATT_ServerTest, ExecuteWriteInvalidFlag) {
+  // Payload is one byte too short.
+  // clang-format off
+  const auto kInvalidPDU = common::CreateStaticByteBuffer(
+      0x18,  // opcode: execute write request
+      0xFF   // flag: invalid
+  );
+  const auto kExpected = common::CreateStaticByteBuffer(
+      0x01,        // opcode: error response
+      0x18,        // request: execute write request
+      0x00, 0x00,  // handle: 0
+      0x04         // error: Invalid PDU
+  );
+  // clang-format on
+
+  EXPECT_TRUE(ReceiveAndExpect(kInvalidPDU, kExpected));
+}
+
+// Tests that an "execute write request" without any prepared writes returns
+// success without writing to any attributes.
+TEST_F(GATT_ServerTest, ExecuteWriteQueueEmpty) {
+  // clang-format off
+  const auto kExecute = common::CreateStaticByteBuffer(
+    0x18,  // opcode: execute write request
+    0x01   // flag: "write pending"
+  );
+  const auto kExecuteResponse = common::CreateStaticByteBuffer(
+    0x19  // opcode: execute write response
+  );
+  // clang-format on
+
+  // |buffer| should contain the partial writes.
+  EXPECT_TRUE(ReceiveAndExpect(kExecute, kExecuteResponse));
+}
+
+TEST_F(GATT_ServerTest, ExecuteWriteSuccess) {
+  auto buffer = common::CreateStaticByteBuffer('x', 'x', 'x', 'x', 'x', 'x');
+
+  auto* grp = db()->NewGrouping(types::kPrimaryService, 1, kTestValue1);
+  auto* attr = grp->AddAttribute(kTestType16, att::AccessRequirements(),
+                                 AllowedNoSecurity());
+  attr->set_write_handler([&](const auto& peer_id, att::Handle handle,
+                              uint16_t offset, const auto& value,
+                              const auto& result_cb) {
+    EXPECT_EQ(kTestDeviceId, peer_id);
+    EXPECT_EQ(attr->handle(), handle);
+
+    // Write the contents into |buffer|.
+    buffer.Write(value, offset);
+    result_cb(att::ErrorCode::kNoError);
+  });
+  grp->set_active(true);
+
+  // Prepare two partial writes of the string "hello!".
+  // clang-format off
+  const auto kPrepare1 = common::CreateStaticByteBuffer(
+    0x016,              // opcode: prepare write request
+    0x02, 0x00,         // handle: 0x0002
+    0x00, 0x00,         // offset: 0
+    'h', 'e', 'l', 'l'  // value: "hell"
+  );
+  const auto kPrepareResponse1 = common::CreateStaticByteBuffer(
+    0x017,              // opcode: prepare write response
+    0x02, 0x00,         // handle: 0x0002
+    0x00, 0x00,         // offset: 0
+    'h', 'e', 'l', 'l'  // value: "hell"
+  );
+  const auto kPrepare2 = common::CreateStaticByteBuffer(
+    0x016,              // opcode: prepare write request
+    0x02, 0x00,         // handle: 0x0002
+    0x04, 0x00,         // offset: 4
+    'o', '!'            // value: "o!"
+  );
+  const auto kPrepareResponse2 = common::CreateStaticByteBuffer(
+    0x017,              // opcode: prepare write response
+    0x02, 0x00,         // handle: 0x0002
+    0x04, 0x00,         // offset: 4
+    'o', '!'            // value: "o!"
+  );
+
+  // Add an overlapping write that partial overwrites data from previous
+  // payloads.
+  const auto kPrepare3 = common::CreateStaticByteBuffer(
+    0x016,              // opcode: prepare write request
+    0x02, 0x00,         // handle: 0x0002
+    0x02, 0x00,         // offset: 2
+    'r', 'p', '?'       // value: "rp?"
+  );
+  const auto kPrepareResponse3 = common::CreateStaticByteBuffer(
+    0x017,              // opcode: prepare write response
+    0x02, 0x00,         // handle: 0x0002
+    0x02, 0x00,         // offset: 2
+    'r', 'p', '?'       // value: "rp?"
+  );
+
+  // clang-format on
+
+  EXPECT_TRUE(ReceiveAndExpect(kPrepare1, kPrepareResponse1));
+  EXPECT_TRUE(ReceiveAndExpect(kPrepare2, kPrepareResponse2));
+  EXPECT_TRUE(ReceiveAndExpect(kPrepare3, kPrepareResponse3));
+
+  // The writes should not be committed yet.
+  EXPECT_EQ("xxxxxx", buffer.AsString());
+
+  // clang-format off
+  const auto kExecute = common::CreateStaticByteBuffer(
+    0x18,  // opcode: execute write request
+    0x01   // flag: "write pending"
+  );
+  const auto kExecuteResponse = common::CreateStaticByteBuffer(
+    0x19  // opcode: execute write response
+  );
+  // clang-format on
+
+  // |buffer| should contain the partial writes.
+  EXPECT_TRUE(ReceiveAndExpect(kExecute, kExecuteResponse));
+  EXPECT_EQ("herp?!", buffer.AsString());
+}
+
+// Tests that the rest of the queue is dropped if a prepared write fails.
+TEST_F(GATT_ServerTest, ExecuteWriteError) {
+  auto buffer = common::CreateStaticByteBuffer('x', 'x', 'x', 'x', 'x', 'x');
+
+  auto* grp = db()->NewGrouping(types::kPrimaryService, 1, kTestValue1);
+  auto* attr = grp->AddAttribute(kTestType16, att::AccessRequirements(),
+                                 AllowedNoSecurity());
+  attr->set_write_handler([&](const auto& peer_id, att::Handle handle,
+                              uint16_t offset, const auto& value,
+                              const auto& result_cb) {
+    EXPECT_EQ(kTestDeviceId, peer_id);
+    EXPECT_EQ(attr->handle(), handle);
+
+    // Make the write to non-zero offsets fail (this corresponds to the second
+    // partial write we prepare below.
+    if (offset) {
+      result_cb(att::ErrorCode::kUnlikelyError);
+    } else {
+      buffer.Write(value);
+      result_cb(att::ErrorCode::kNoError);
+    }
+  });
+  grp->set_active(true);
+
+  // Prepare two partial writes of the string "hello!".
+  // clang-format off
+  const auto kPrepare1 = common::CreateStaticByteBuffer(
+    0x016,              // opcode: prepare write request
+    0x02, 0x00,         // handle: 0x0002
+    0x00, 0x00,         // offset: 0
+    'h', 'e', 'l', 'l'  // value: "hell"
+  );
+  const auto kPrepareResponse1 = common::CreateStaticByteBuffer(
+    0x017,              // opcode: prepare write response
+    0x02, 0x00,         // handle: 0x0002
+    0x00, 0x00,         // offset: 0
+    'h', 'e', 'l', 'l'  // value: "hell"
+  );
+  const auto kPrepare2 = common::CreateStaticByteBuffer(
+    0x016,              // opcode: prepare write request
+    0x02, 0x00,         // handle: 0x0002
+    0x04, 0x00,         // offset: 4
+    'o', '!'            // value: "o!"
+  );
+  const auto kPrepareResponse2 = common::CreateStaticByteBuffer(
+    0x017,              // opcode: prepare write response
+    0x02, 0x00,         // handle: 0x0002
+    0x04, 0x00,         // offset: 4
+    'o', '!'            // value: "o!"
+  );
+  // clang-format on
+
+  EXPECT_TRUE(ReceiveAndExpect(kPrepare1, kPrepareResponse1));
+  EXPECT_TRUE(ReceiveAndExpect(kPrepare2, kPrepareResponse2));
+
+  // The writes should not be committed yet.
+  EXPECT_EQ("xxxxxx", buffer.AsString());
+
+  // clang-format off
+  const auto kExecute = common::CreateStaticByteBuffer(
+    0x18,  // opcode: execute write request
+    0x01   // flag: "write pending"
+  );
+  const auto kExecuteResponse = common::CreateStaticByteBuffer(
+    0x01,        // opcode: error response
+    0x18,        // request: execute write request
+    0x02, 0x00,  // handle: 2 (the attribute in error)
+    0x0E         // error: Unlikely Error (returned by callback above).
+  );
+  // clang-format on
+
+  // Only the first partial write should have gone through as the second one
+  // is expected to fail.
+  EXPECT_TRUE(ReceiveAndExpect(kExecute, kExecuteResponse));
+  EXPECT_EQ("hellxx", buffer.AsString());
+}
+
+TEST_F(GATT_ServerTest, ExecuteWriteAbort) {
+  auto* grp = db()->NewGrouping(types::kPrimaryService, 1, kTestValue1);
+  // |attr| has handle "2".
+  auto* attr = grp->AddAttribute(kTestType16, att::AccessRequirements(),
+                                 AllowedNoSecurity());
+
+  int write_count = 0;
+  attr->set_write_handler([&](const auto& peer_id, att::Handle handle,
+                              uint16_t offset, const auto& value,
+                              const auto& result_cb) {
+    write_count++;
+
+    EXPECT_EQ(kTestDeviceId, peer_id);
+    EXPECT_EQ(attr->handle(), handle);
+    EXPECT_EQ(0u, offset);
+    EXPECT_TRUE(common::ContainersEqual(
+        common::CreateStaticByteBuffer('l', 'o', 'l'), value));
+    result_cb(att::ErrorCode::kNoError);
+  });
+  grp->set_active(true);
+
+  // clang-format off
+  const auto kPrepareToAbort = common::CreateStaticByteBuffer(
+    0x016,              // opcode: prepare write request
+    0x02, 0x00,         // handle: 0x0002
+    0x00, 0x00,         // offset: 0
+    't', 'e', 's', 't'  // value: "test"
+  );
+  const auto kPrepareToAbortResponse = common::CreateStaticByteBuffer(
+    0x017,              // opcode: prepare write response
+    0x02, 0x00,         // handle: 0x0002
+    0x00, 0x00,         // offset: 0
+    't', 'e', 's', 't'  // value: "test"
+  );
+  // clang-format on
+
+  // Prepare writes. These should get committed right away.
+  EXPECT_TRUE(ReceiveAndExpect(kPrepareToAbort, kPrepareToAbortResponse));
+  EXPECT_TRUE(ReceiveAndExpect(kPrepareToAbort, kPrepareToAbortResponse));
+  EXPECT_TRUE(ReceiveAndExpect(kPrepareToAbort, kPrepareToAbortResponse));
+  EXPECT_TRUE(ReceiveAndExpect(kPrepareToAbort, kPrepareToAbortResponse));
+  EXPECT_EQ(0, write_count);
+
+  // Abort the writes. They should get dropped.
+  // clang-format off
+  const auto kAbort = common::CreateStaticByteBuffer(
+    0x18,  // opcode: execute write request
+    0x00   // flag: "cancel all"
+  );
+  const auto kAbortResponse = common::CreateStaticByteBuffer(
+    0x19  // opcode: execute write response
+  );
+  // clang-format on
+  EXPECT_TRUE(ReceiveAndExpect(kAbort, kAbortResponse));
+  EXPECT_EQ(0, write_count);
+
+  // Prepare and commit a new write request. This one should take effect without
+  // involving the previously aborted writes.
+  // clang-format off
+  const auto kPrepareToCommit = common::CreateStaticByteBuffer(
+    0x016,              // opcode: prepare write request
+    0x02, 0x00,         // handle: 0x0002
+    0x00, 0x00,         // offset: 0
+    'l', 'o', 'l'       // value: "lol"
+  );
+  const auto kPrepareToCommitResponse = common::CreateStaticByteBuffer(
+    0x017,              // opcode: prepare write response
+    0x02, 0x00,         // handle: 0x0002
+    0x00, 0x00,         // offset: 0
+    'l', 'o', 'l'       // value: "lol"
+  );
+  const auto kCommit = common::CreateStaticByteBuffer(
+    0x18,  // opcode: execute write request
+    0x01   // flag: "write pending"
+  );
+  const auto kCommitResponse = common::CreateStaticByteBuffer(
+    0x19  // opcode: execute write response
+  );
+  // clang-format on
+
+  EXPECT_TRUE(ReceiveAndExpect(kPrepareToCommit, kPrepareToCommitResponse));
+  EXPECT_TRUE(ReceiveAndExpect(kCommit, kCommitResponse));
+  EXPECT_EQ(1, write_count);
+}
+
 TEST_F(GATT_ServerTest, SendNotificationEmpty) {
   constexpr att::Handle kHandle = 0x1234;
   const common::BufferView kTestValue;
@@ -2119,6 +2534,26 @@ class GATT_ServerTest_Security : public GATT_ServerTest {
     }
   }
 
+  bool EmulatePrepareWriteRequest(att::Handle handle,
+                                  att::ErrorCode expected_ecode) {
+    fake_chan()->Receive(common::CreateStaticByteBuffer(
+        0x16,                                  // opcode: prepare write request
+        LowerBits(handle), UpperBits(handle),  // handle
+        0x00, 0x00,                            // offset: 0
+        't', 'e', 's', 't'                     // value: "test"
+        ));
+    if (expected_ecode == att::ErrorCode::kNoError) {
+      return Expect(common::CreateStaticByteBuffer(
+          0x17,                                  // prepare write response
+          LowerBits(handle), UpperBits(handle),  // handle
+          0x00, 0x00,                            // offset: 0
+          't', 'e', 's', 't'                     // value: "test"
+          ));
+    } else {
+      return ExpectAttError(0x16, handle, expected_ecode);
+    }
+  }
+
   // Emulates the receipt of a Write Command. The expected error code parameter
   // is unused since ATT commands do not have a response.
   bool EmulateWriteCommand(att::Handle handle, att::ErrorCode) {
@@ -2191,6 +2626,9 @@ class GATT_ServerTest_Security : public GATT_ServerTest {
   void RunWriteRequestTest() {
     RunTest<&GATT_ServerTest_Security::EmulateWriteRequest, true>();
   }
+  void RunPrepareWriteRequestTest() {
+    RunTest<&GATT_ServerTest_Security::EmulatePrepareWriteRequest, true>();
+  }
   void RunWriteCommandTest() {
     RunTest<&GATT_ServerTest_Security::EmulateWriteCommand, true>();
   }
@@ -2251,6 +2689,15 @@ TEST_F(GATT_ServerTest_Security, WriteCommandErrorSecurity) {
   EXPECT_EQ(4u, write_count());
 }
 
+TEST_F(GATT_ServerTest_Security, PrepareWriteRequestSecurity) {
+  InitializeAttributesForWriting();
+  RunPrepareWriteRequestTest();
+
+  // None of the write handlers should have been called since no execute write
+  // request has been sent.
+  EXPECT_EQ(0u, write_count());
+}
+
 }  // namespace
 }  // namespace gatt
 }  // namespace bt