diff --git a/.gitignore b/.gitignore
index d56046f8a..96fd7c3fa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,6 +39,38 @@ TAGS
 *.capnp.h
 
 # LCP generated C++ & Cap'n Proto files
-src/query/plan/operator.hpp
-src/query/plan/operator.lcp.cpp
+*.lcp.cpp
+src/database/counters_rpc_messages.capnp
+src/database/counters_rpc_messages.hpp
+src/database/state_delta.capnp
+src/database/state_delta.hpp
+src/distributed/bfs_rpc_messages.capnp
+src/distributed/bfs_rpc_messages.hpp
+src/distributed/coordination_rpc_messages.capnp
+src/distributed/coordination_rpc_messages.hpp
+src/distributed/data_rpc_messages.capnp
+src/distributed/data_rpc_messages.hpp
+src/distributed/durability_rpc_messages.capnp
+src/distributed/durability_rpc_messages.hpp
+src/distributed/index_rpc_messages.capnp
+src/distributed/index_rpc_messages.hpp
+src/distributed/plan_rpc_messages.capnp
+src/distributed/plan_rpc_messages.hpp
+src/distributed/pull_produce_rpc_messages.capnp
+src/distributed/pull_produce_rpc_messages.hpp
+src/distributed/storage_gc_rpc_messages.capnp
+src/distributed/storage_gc_rpc_messages.hpp
+src/distributed/token_sharing_rpc_messages.capnp
+src/distributed/token_sharing_rpc_messages.hpp
+src/distributed/transactional_cache_cleaner_rpc_messages.capnp
+src/distributed/transactional_cache_cleaner_rpc_messages.hpp
+src/distributed/updates_rpc_messages.capnp
+src/distributed/updates_rpc_messages.hpp
 src/query/plan/operator.capnp
+src/query/plan/operator.hpp
+src/stats/stats_rpc_messages.capnp
+src/stats/stats_rpc_messages.hpp
+src/storage/concurrent_id_mapper_rpc_messages.capnp
+src/storage/concurrent_id_mapper_rpc_messages.hpp
+src/transactions/engine_rpc_messages.capnp
+src/transactions/engine_rpc_messages.hpp
diff --git a/init b/init
index 5d50345ee..8b09f86eb 100755
--- a/init
+++ b/init
@@ -111,6 +111,7 @@ fi
 mkdir -p ./build
 
 # quicklisp package manager for Common Lisp
+# TODO: We should at some point cache or have a mirror of packages we use.
 quicklisp_install_dir="$HOME/quicklisp"
 if [[ -v QUICKLISP_HOME ]]; then
   quicklisp_install_dir="${QUICKLISP_HOME}"
@@ -121,6 +122,7 @@ if [[ ! -f "${quicklisp_install_dir}/setup.lisp" ]]; then
   "
   (load \"${DIR}/quicklisp.lisp\")
   (quicklisp-quickstart:install :path \"${quicklisp_install_dir}\")
+  (ql:quickload :cl-ppcre :silent t)
   " | sbcl --script || exit 1
   rm -rf quicklisp.lisp || exit 1
 fi
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index e169c062b..c7ed82cc9 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -35,6 +35,7 @@ set(memgraph_src_files
     distributed/data_rpc_server.cpp
     distributed/produce_rpc_server.cpp
     distributed/pull_rpc_clients.cpp
+    distributed/serialization.cpp
     distributed/updates_rpc_clients.cpp
     distributed/updates_rpc_server.cpp
     durability/paths.cpp
@@ -70,6 +71,7 @@ set(memgraph_src_files
     transactions/engine_master.cpp
     transactions/engine_single_node.cpp
     transactions/engine_worker.cpp
+    transactions/snapshot.cpp
 )
 # -----------------------------------------------------------------------------
 
@@ -96,23 +98,31 @@ set(lcp_src_files lisp/lcp.lisp ${lcp_exe})
 
 # Use this function to add each lcp file to generation. This way each file is
 # standalone and we avoid recompiling everything.
+#
+# You may pass a CAPNP_SCHEMA <id> keyword argument to generate the Cap'n Proto
+# serialization code from .lcp file. You still need to add the generated capnp
+# file through `add_capnp` function. To generate the <id> use `capnp id`
+# invocation, and specify it here. This preserves correct id information across
+# multiple schema generations. If this wasn't the case, wrong typeId
+# information will break RPC between different compilations of memgraph.
+#
 # NOTE: memgraph_src_files and generated_lcp_files are globally updated.
 function(add_lcp lcp_file)
-  set(options CAPNP_SCHEMA)
-  cmake_parse_arguments(KW "${options}" "" "" ${ARGN})
+  set(one_value_kwargs CAPNP_SCHEMA)
+  cmake_parse_arguments(KW "" "${one_value_kwargs}" "" ${ARGN})
   string(REGEX REPLACE "\.lcp$" ".hpp" h_file
          "${CMAKE_CURRENT_SOURCE_DIR}/${lcp_file}")
   if (KW_CAPNP_SCHEMA)
     string(REGEX REPLACE "\.lcp$" ".capnp" capnp_file
            "${CMAKE_CURRENT_SOURCE_DIR}/${lcp_file}")
-    set(capnp_id_command ${CAPNP_EXE})
+    set(capnp_id ${KW_CAPNP_SCHEMA})
     set(capnp_depend capnproto-proj)
     set(cpp_file ${CMAKE_CURRENT_SOURCE_DIR}/${lcp_file}.cpp)
     # Update *global* memgraph_src_files
     set(memgraph_src_files ${memgraph_src_files} ${cpp_file} PARENT_SCOPE)
   endif()
   add_custom_command(OUTPUT ${h_file} ${cpp_file} ${capnp_file}
-    COMMAND ${lcp_exe} ${lcp_file} ${capnp_id_command}
+    COMMAND ${lcp_exe} ${lcp_file} ${capnp_id}
     VERBATIM
     DEPENDS ${lcp_file} ${lcp_src_files} ${capnp_depend}
     WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
@@ -120,18 +130,54 @@ function(add_lcp lcp_file)
   set(generated_lcp_files ${generated_lcp_files} ${h_file} ${cpp_file} ${capnp_file} PARENT_SCOPE)
 endfunction(add_lcp)
 
-add_lcp(query/plan/operator.lcp CAPNP_SCHEMA)
+add_lcp(database/counters_rpc_messages.lcp CAPNP_SCHEMA @0x95a2c3ea3871e945)
+add_capnp(database/counters_rpc_messages.capnp)
+add_lcp(database/state_delta.lcp CAPNP_SCHEMA @0xdea01657b3563887)
+add_capnp(database/state_delta.capnp)
+add_lcp(distributed/bfs_rpc_messages.lcp CAPNP_SCHEMA @0x8e508640b09b6d2a)
+add_capnp(distributed/bfs_rpc_messages.capnp)
+add_lcp(distributed/coordination_rpc_messages.lcp CAPNP_SCHEMA @0x93df0c4703cf98fb)
+add_capnp(distributed/coordination_rpc_messages.capnp)
+add_lcp(distributed/data_rpc_messages.lcp CAPNP_SCHEMA @0xc1c8a341ba37aaf5)
+add_capnp(distributed/data_rpc_messages.capnp)
+add_lcp(distributed/durability_rpc_messages.lcp CAPNP_SCHEMA @0xf5e53bc271e2163d)
+add_capnp(distributed/durability_rpc_messages.capnp)
+add_lcp(distributed/index_rpc_messages.lcp CAPNP_SCHEMA @0xa8aab46862945bd6)
+add_capnp(distributed/index_rpc_messages.capnp)
+add_lcp(distributed/plan_rpc_messages.lcp CAPNP_SCHEMA @0xfcbc48dc9f106d28)
+add_capnp(distributed/plan_rpc_messages.capnp)
+add_lcp(distributed/pull_produce_rpc_messages.lcp CAPNP_SCHEMA @0xa78a9254a73685bd)
+add_capnp(distributed/pull_produce_rpc_messages.capnp)
+add_lcp(distributed/storage_gc_rpc_messages.lcp CAPNP_SCHEMA @0xd705663dfe36cf81)
+add_capnp(distributed/storage_gc_rpc_messages.capnp)
+add_lcp(distributed/token_sharing_rpc_messages.lcp CAPNP_SCHEMA @0x8f295db54ec4caec)
+add_capnp(distributed/token_sharing_rpc_messages.capnp)
+add_lcp(distributed/transactional_cache_cleaner_rpc_messages.lcp CAPNP_SCHEMA @0xe2be6183a1ff9e11)
+add_capnp(distributed/transactional_cache_cleaner_rpc_messages.capnp)
+add_lcp(distributed/updates_rpc_messages.lcp CAPNP_SCHEMA @0x82d5f38d73c7b53a)
+add_capnp(distributed/updates_rpc_messages.capnp)
+add_lcp(query/plan/operator.lcp CAPNP_SCHEMA @0xe5cae8d045d30c42)
 add_capnp(query/plan/operator.capnp)
+add_lcp(stats/stats_rpc_messages.lcp CAPNP_SCHEMA @0xc19a87c81b9b4512)
+add_capnp(stats/stats_rpc_messages.capnp)
+add_lcp(storage/concurrent_id_mapper_rpc_messages.lcp CAPNP_SCHEMA @0xa6068dae93d225dd)
+add_capnp(storage/concurrent_id_mapper_rpc_messages.capnp)
+add_lcp(transactions/engine_rpc_messages.lcp CAPNP_SCHEMA @0xde02b7c49180cad5)
+add_capnp(transactions/engine_rpc_messages.capnp)
 
 add_custom_target(generate_lcp DEPENDS ${generated_lcp_files})
 
 # Registering capnp must come after registering lcp files.
 
-add_capnp(query/frontend/semantic/symbol.capnp)
-add_capnp(query/frontend/ast/ast.capnp)
-add_capnp(utils/serialization.capnp)
-add_capnp(storage/types.capnp)
+add_capnp(communication/rpc/messages.capnp)
+add_capnp(distributed/serialization.capnp)
+add_capnp(durability/recovery.capnp)
 add_capnp(query/common.capnp)
+add_capnp(query/frontend/ast/ast.capnp)
+add_capnp(query/frontend/semantic/symbol.capnp)
+add_capnp(storage/serialization.capnp)
+add_capnp(transactions/common.capnp)
+add_capnp(utils/serialization.capnp)
 
 add_custom_target(generate_capnp DEPENDS generate_lcp ${generated_capnp_files})
 
diff --git a/src/communication/raft/network_common.hpp b/src/communication/raft/network_common.hpp
index 347b98df5..96ceeb0b0 100644
--- a/src/communication/raft/network_common.hpp
+++ b/src/communication/raft/network_common.hpp
@@ -1,8 +1,5 @@
 #pragma once
 
-#include "boost/serialization/access.hpp"
-#include "boost/serialization/base_object.hpp"
-
 #include "communication/rpc/messages.hpp"
 #include "communication/raft/raft.hpp"
 
@@ -11,38 +8,16 @@ namespace communication::raft {
 enum class RpcType { REQUEST_VOTE, APPEND_ENTRIES };
 
 template <class State>
-struct PeerRpcRequest : public rpc::Message {
+struct PeerRpcRequest {
   RpcType type;
   RequestVoteRequest request_vote;
   AppendEntriesRequest<State> append_entries;
-
- private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<rpc::Message>(*this);
-    ar &type;
-    ar &request_vote;
-    ar &append_entries;
-  }
 };
 
-struct PeerRpcReply : public rpc::Message {
+struct PeerRpcReply {
   RpcType type;
   RequestVoteReply request_vote;
   AppendEntriesReply append_entries;
-
- private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<rpc::Message>(*this);
-    ar &type;
-    ar &request_vote;
-    ar &append_entries;
-  }
 };
 
 }  // namespace communication::raft
diff --git a/src/communication/raft/rpc.hpp b/src/communication/raft/rpc.hpp
index 4dddf66d5..1cd5bb7f3 100644
--- a/src/communication/raft/rpc.hpp
+++ b/src/communication/raft/rpc.hpp
@@ -30,24 +30,27 @@ class RpcNetwork : public RaftNetworkInterface<State> {
       : server_(server), directory_(std::move(directory)) {}
 
   virtual void Start(RaftMember<State> &member) override {
-    server_.Register<PeerProtocol<State>>(
-        [&member](const PeerRpcRequest<State> &request) {
-          auto reply = std::make_unique<PeerRpcReply>();
-          reply->type = request.type;
-          switch (request.type) {
-            case RpcType::REQUEST_VOTE:
-              reply->request_vote = member.OnRequestVote(request.request_vote);
-              break;
-            case RpcType::APPEND_ENTRIES:
-              reply->append_entries =
-                  member.OnAppendEntries(request.append_entries);
-              break;
-            default:
-              LOG(ERROR) << "Unknown RPC type: "
-                         << static_cast<int>(request.type);
-          }
-          return reply;
-        });
+    // TODO: Serialize RPC via Cap'n Proto
+//    server_.Register<PeerProtocol<State>>(
+//        [&member](const auto &req_reader, auto *res_builder) {
+//          PeerRpcRequest<State> request;
+//          request.Load(req_reader);
+//          PeerRpcReply reply;
+//          reply.type = request.type;
+//          switch (request.type) {
+//            case RpcType::REQUEST_VOTE:
+//              reply.request_vote = member.OnRequestVote(request.request_vote);
+//              break;
+//            case RpcType::APPEND_ENTRIES:
+//              reply.append_entries =
+//                  member.OnAppendEntries(request.append_entries);
+//              break;
+//            default:
+//              LOG(ERROR) << "Unknown RPC type: "
+//                         << static_cast<int>(request.type);
+//          }
+//          reply.Save(res_builder);
+//        });
   }
 
   virtual bool SendRequestVote(const MemberId &recipient,
diff --git a/src/communication/rpc/client.cpp b/src/communication/rpc/client.cpp
index aa4498a29..0d99740bf 100644
--- a/src/communication/rpc/client.cpp
+++ b/src/communication/rpc/client.cpp
@@ -1,12 +1,6 @@
 #include <chrono>
 #include <thread>
 
-#include "boost/archive/binary_iarchive.hpp"
-#include "boost/archive/binary_oarchive.hpp"
-#include "boost/serialization/access.hpp"
-#include "boost/serialization/base_object.hpp"
-#include "boost/serialization/export.hpp"
-#include "boost/serialization/unique_ptr.hpp"
 #include "gflags/gflags.h"
 
 #include "communication/rpc/client.hpp"
@@ -19,7 +13,8 @@ namespace communication::rpc {
 
 Client::Client(const io::network::Endpoint &endpoint) : endpoint_(endpoint) {}
 
-std::unique_ptr<Message> Client::Call(const Message &request) {
+std::experimental::optional<::capnp::FlatArrayMessageReader> Client::Send(
+    ::capnp::MessageBuilder *message) {
   std::lock_guard<std::mutex> guard(mutex_);
 
   if (FLAGS_rpc_random_latency) {
@@ -39,45 +34,37 @@ std::unique_ptr<Message> Client::Call(const Message &request) {
     if (!client_->Connect(endpoint_)) {
       LOG(ERROR) << "Couldn't connect to remote address " << endpoint_;
       client_ = std::experimental::nullopt;
-      return nullptr;
+      return std::experimental::nullopt;
     }
   }
 
   // Serialize and send request.
-  std::stringstream request_stream(std::ios_base::out | std::ios_base::binary);
-  {
-    boost::archive::binary_oarchive request_archive(request_stream);
-    // Serialize reference as pointer (to serialize the derived class). The
-    // request is read in protocol.cpp.
-    request_archive << &request;
-    // Archive destructor ensures everything is written.
-  }
-
-  const std::string &request_buffer = request_stream.str();
-  CHECK(request_buffer.size() <= std::numeric_limits<MessageSize>::max())
+  auto request_words = ::capnp::messageToFlatArray(*message);
+  auto request_bytes = request_words.asBytes();
+  CHECK(request_bytes.size() <= std::numeric_limits<MessageSize>::max())
       << fmt::format(
              "Trying to send message of size {}, max message size is {}",
-             request_buffer.size(), std::numeric_limits<MessageSize>::max());
+             request_bytes.size(), std::numeric_limits<MessageSize>::max());
 
-  MessageSize request_data_size = request_buffer.size();
+  MessageSize request_data_size = request_bytes.size();
   if (!client_->Write(reinterpret_cast<uint8_t *>(&request_data_size),
                       sizeof(MessageSize), true)) {
     LOG(ERROR) << "Couldn't send request size to " << client_->endpoint();
     client_ = std::experimental::nullopt;
-    return nullptr;
+    return std::experimental::nullopt;
   }
 
-  if (!client_->Write(request_buffer)) {
+  if (!client_->Write(request_bytes.begin(), request_bytes.size())) {
     LOG(ERROR) << "Couldn't send request data to " << client_->endpoint();
     client_ = std::experimental::nullopt;
-    return nullptr;
+    return std::experimental::nullopt;
   }
 
   // Receive response data size.
   if (!client_->Read(sizeof(MessageSize))) {
     LOG(ERROR) << "Couldn't get response from " << client_->endpoint();
     client_ = std::experimental::nullopt;
-    return nullptr;
+    return std::experimental::nullopt;
   }
   MessageSize response_data_size =
       *reinterpret_cast<MessageSize *>(client_->GetData());
@@ -87,22 +74,19 @@ std::unique_ptr<Message> Client::Call(const Message &request) {
   if (!client_->Read(response_data_size)) {
     LOG(ERROR) << "Couldn't get response from " << client_->endpoint();
     client_ = std::experimental::nullopt;
-    return nullptr;
-  }
-
-  std::unique_ptr<Message> response;
-  {
-    std::stringstream response_stream(std::ios_base::in |
-                                      std::ios_base::binary);
-    response_stream.str(std::string(reinterpret_cast<char *>(client_->GetData()),
-                                    response_data_size));
-    boost::archive::binary_iarchive response_archive(response_stream);
-    response_archive >> response;
+    return std::experimental::nullopt;
   }
 
+  // Read the response message.
+  auto data = ::kj::arrayPtr(client_->GetData(), response_data_size);
+  // Our data is word aligned and padded to 64bit because we use regular
+  // (non-packed) serialization of Cap'n Proto. So we can use reinterpret_cast.
+  auto data_words =
+      ::kj::arrayPtr(reinterpret_cast<::capnp::word *>(data.begin()),
+                     reinterpret_cast<::capnp::word *>(data.end()));
+  ::capnp::FlatArrayMessageReader response_message(data_words.asConst());
   client_->ShiftData(response_data_size);
-
-  return response;
+  return std::experimental::make_optional(std::move(response_message));
 }
 
 void Client::Abort() {
diff --git a/src/communication/rpc/client.hpp b/src/communication/rpc/client.hpp
index b5712028e..47d7cffb4 100644
--- a/src/communication/rpc/client.hpp
+++ b/src/communication/rpc/client.hpp
@@ -5,62 +5,85 @@
 #include <mutex>
 #include <random>
 
+#include <capnp/message.h>
+#include <capnp/serialize.h>
 #include <glog/logging.h>
 
 #include "communication/client.hpp"
+#include "communication/rpc/messages.capnp.h"
 #include "communication/rpc/messages.hpp"
 #include "io/network/endpoint.hpp"
 #include "utils/demangle.hpp"
 
 namespace communication::rpc {
 
-// Client is thread safe, but it is recommended to use thread_local clients.
+/// Client is thread safe, but it is recommended to use thread_local clients.
 class Client {
  public:
-  Client(const io::network::Endpoint &endpoint);
+  explicit Client(const io::network::Endpoint &endpoint);
 
-  // Call function can initiate only one request at the time. Function blocks
-  // until there is a response. If there was an error nullptr is returned.
-  template <typename TRequestResponse, typename... Args>
-  std::unique_ptr<typename TRequestResponse::Response> Call(Args &&... args) {
-    using Req = typename TRequestResponse::Request;
-    using Res = typename TRequestResponse::Response;
-    static_assert(std::is_base_of<Message, Req>::value,
-                  "TRequestResponse::Request must be derived from Message");
-    static_assert(std::is_base_of<Message, Res>::value,
-                  "TRequestResponse::Response must be derived from Message");
-    auto request = Req(std::forward<Args>(args)...);
+  /// Call function can initiate only one request at the time. Function blocks
+  /// until there is a response. If there was an error nullptr is returned.
+  template <class TRequestResponse, class... Args>
+  std::experimental::optional<typename TRequestResponse::Response> Call(
+      Args &&... args) {
+    return CallWithLoad<TRequestResponse>(
+        [](const auto &reader) {
+          typename TRequestResponse::Response response;
+          response.Load(reader);
+          return response;
+        },
+        std::forward<Args>(args)...);
+  }
 
-    if (VLOG_IS_ON(12)) {
-      auto req_type = utils::Demangle(request.type_index().name());
-      LOG(INFO) << "[RpcClient] sent " << (req_type ? req_type.value() : "");
+  /// Same as `Call` but the first argument is a response loading function.
+  template <class TRequestResponse, class... Args>
+  std::experimental::optional<typename TRequestResponse::Response> CallWithLoad(
+      std::function<typename TRequestResponse::Response(
+          const typename TRequestResponse::Response::Capnp::Reader &)>
+          load,
+      Args &&... args) {
+    typename TRequestResponse::Request request(std::forward<Args>(args)...);
+    auto req_type = TRequestResponse::Request::TypeInfo;
+    VLOG(12) << "[RpcClient] sent " << req_type.name;
+    ::capnp::MallocMessageBuilder req_msg;
+    {
+      auto builder = req_msg.initRoot<capnp::Message>();
+      builder.setTypeId(req_type.id);
+      auto data_builder = builder.initData();
+      auto req_builder =
+          data_builder
+              .template initAs<typename TRequestResponse::Request::Capnp>();
+      request.Save(&req_builder);
     }
-
-    std::unique_ptr<Message> response = Call(request);
-    auto *real_response = dynamic_cast<Res *>(response.get());
-    if (!real_response && response) {
+    auto maybe_response = Send(&req_msg);
+    if (!maybe_response) {
+      return std::experimental::nullopt;
+    }
+    auto res_msg = maybe_response->getRoot<capnp::Message>();
+    auto res_type = TRequestResponse::Response::TypeInfo;
+    if (res_msg.getTypeId() != res_type.id) {
       // Since message_id was checked in private Call function, this means
       // something is very wrong (probably on the server side).
       LOG(ERROR) << "Message response was of unexpected type";
       client_ = std::experimental::nullopt;
-      return nullptr;
+      return std::experimental::nullopt;
     }
 
-    if (VLOG_IS_ON(12) && response) {
-      auto res_type = utils::Demangle(response->type_index().name());
-      LOG(INFO) << "[RpcClient] received "
-                << (res_type ? res_type.value() : "");
-    }
+    VLOG(12) << "[RpcClient] received " << res_type.name;
 
-    response.release();
-    return std::unique_ptr<Res>(real_response);
+    auto data_reader =
+        res_msg.getData()
+            .template getAs<typename TRequestResponse::Response::Capnp>();
+    return std::experimental::make_optional(load(data_reader));
   }
 
-  // Call this function from another thread to abort a pending RPC call.
+  /// Call this function from another thread to abort a pending RPC call.
   void Abort();
 
  private:
-  std::unique_ptr<Message> Call(const Message &request);
+  std::experimental::optional<::capnp::FlatArrayMessageReader> Send(
+      ::capnp::MessageBuilder *message);
 
   io::network::Endpoint endpoint_;
   std::experimental::optional<communication::Client> client_;
diff --git a/src/communication/rpc/client_pool.hpp b/src/communication/rpc/client_pool.hpp
index dbdf23d64..bfd609abc 100644
--- a/src/communication/rpc/client_pool.hpp
+++ b/src/communication/rpc/client_pool.hpp
@@ -14,10 +14,33 @@ namespace communication::rpc {
  */
 class ClientPool {
  public:
-  ClientPool(const io::network::Endpoint &endpoint) : endpoint_(endpoint) {}
+  explicit ClientPool(const io::network::Endpoint &endpoint)
+      : endpoint_(endpoint) {}
 
-  template <typename TRequestResponse, typename... Args>
-  std::unique_ptr<typename TRequestResponse::Response> Call(Args &&... args) {
+  template <class TRequestResponse, class... Args>
+  std::experimental::optional<typename TRequestResponse::Response> Call(
+      Args &&... args) {
+    return WithUnusedClient([&](const auto &client) {
+      return client->template Call<TRequestResponse>(
+          std::forward<Args>(args)...);
+    });
+  };
+
+  template <class TRequestResponse, class... Args>
+  std::experimental::optional<typename TRequestResponse::Response> CallWithLoad(
+      std::function<typename TRequestResponse::Response(
+          const typename TRequestResponse::Response::Capnp::Reader &)>
+          load,
+      Args &&... args) {
+    return WithUnusedClient([&](const auto &client) {
+      return client->template CallWithLoad<TRequestResponse>(
+          load, std::forward<Args>(args)...);
+    });
+  };
+
+ private:
+  template <class TFun>
+  auto WithUnusedClient(const TFun &fun) {
     std::unique_ptr<Client> client;
 
     std::unique_lock<std::mutex> lock(mutex_);
@@ -29,14 +52,13 @@ class ClientPool {
     }
     lock.unlock();
 
-    auto resp = client->Call<TRequestResponse>(std::forward<Args>(args)...);
+    auto res = fun(client);
 
     lock.lock();
     unused_clients_.push(std::move(client));
-    return resp;
-  };
+    return res;
+  }
 
- private:
   io::network::Endpoint endpoint_;
 
   std::mutex mutex_;
diff --git a/src/communication/rpc/messages-inl.hpp b/src/communication/rpc/messages-inl.hpp
deleted file mode 100644
index 59f93bff6..000000000
--- a/src/communication/rpc/messages-inl.hpp
+++ /dev/null
@@ -1,163 +0,0 @@
-#pragma once
-
-#include "boost/archive/binary_iarchive.hpp"
-#include "boost/archive/binary_oarchive.hpp"
-#include "boost/serialization/export.hpp"
-
-#include "database/state_delta.hpp"
-#include "distributed/bfs_rpc_messages.hpp"
-#include "distributed/coordination_rpc_messages.hpp"
-#include "distributed/data_rpc_messages.hpp"
-#include "distributed/durability_rpc_messages.hpp"
-#include "distributed/index_rpc_messages.hpp"
-#include "distributed/plan_rpc_messages.hpp"
-#include "distributed/pull_produce_rpc_messages.hpp"
-#include "distributed/storage_gc_rpc_messages.hpp"
-#include "distributed/transactional_cache_cleaner_rpc_messages.hpp"
-#include "distributed/updates_rpc_messages.hpp"
-#include "durability/recovery.hpp"
-#include "stats/stats_rpc_messages.hpp"
-#include "storage/concurrent_id_mapper_rpc_messages.hpp"
-#include "transactions/engine_rpc_messages.hpp"
-
-#define ID_VALUE_EXPORT_BOOST_TYPE(type)      \
-  BOOST_CLASS_EXPORT(storage::type##IdReq);   \
-  BOOST_CLASS_EXPORT(storage::type##IdRes);   \
-  BOOST_CLASS_EXPORT(storage::Id##type##Req); \
-  BOOST_CLASS_EXPORT(storage::Id##type##Res);
-
-ID_VALUE_EXPORT_BOOST_TYPE(Label)
-ID_VALUE_EXPORT_BOOST_TYPE(EdgeType)
-ID_VALUE_EXPORT_BOOST_TYPE(Property)
-
-#undef ID_VALUE_EXPORT_BOOST_TYPE
-
-// Distributed transaction engine.
-BOOST_CLASS_EXPORT(tx::TxAndSnapshot);
-BOOST_CLASS_EXPORT(tx::BeginReq);
-BOOST_CLASS_EXPORT(tx::BeginRes);
-BOOST_CLASS_EXPORT(tx::AdvanceReq);
-BOOST_CLASS_EXPORT(tx::AdvanceRes);
-BOOST_CLASS_EXPORT(tx::CommitReq);
-BOOST_CLASS_EXPORT(tx::CommitRes);
-BOOST_CLASS_EXPORT(tx::AbortReq);
-BOOST_CLASS_EXPORT(tx::AbortRes);
-BOOST_CLASS_EXPORT(tx::SnapshotReq);
-BOOST_CLASS_EXPORT(tx::SnapshotRes);
-BOOST_CLASS_EXPORT(tx::CommandReq);
-BOOST_CLASS_EXPORT(tx::CommandRes);
-BOOST_CLASS_EXPORT(tx::GcSnapshotReq);
-BOOST_CLASS_EXPORT(tx::ClogInfoReq);
-BOOST_CLASS_EXPORT(tx::ClogInfoRes);
-BOOST_CLASS_EXPORT(tx::ActiveTransactionsReq);
-BOOST_CLASS_EXPORT(tx::EnsureNextIdGreaterReq);
-BOOST_CLASS_EXPORT(tx::EnsureNextIdGreaterRes);
-BOOST_CLASS_EXPORT(tx::GlobalLastReq);
-BOOST_CLASS_EXPORT(tx::GlobalLastRes);
-
-// Distributed coordination.
-BOOST_CLASS_EXPORT(durability::RecoveryInfo);
-BOOST_CLASS_EXPORT(distributed::RegisterWorkerReq);
-BOOST_CLASS_EXPORT(distributed::RegisterWorkerRes);
-BOOST_CLASS_EXPORT(distributed::ClusterDiscoveryReq);
-BOOST_CLASS_EXPORT(distributed::ClusterDiscoveryRes);
-BOOST_CLASS_EXPORT(distributed::StopWorkerReq);
-BOOST_CLASS_EXPORT(distributed::StopWorkerRes);
-BOOST_CLASS_EXPORT(distributed::NotifyWorkerRecoveredReq);
-BOOST_CLASS_EXPORT(distributed::NotifyWorkerRecoveredRes);
-
-// Distributed data exchange.
-BOOST_CLASS_EXPORT(distributed::EdgeReq);
-BOOST_CLASS_EXPORT(distributed::EdgeRes);
-BOOST_CLASS_EXPORT(distributed::VertexReq);
-BOOST_CLASS_EXPORT(distributed::VertexRes);
-BOOST_CLASS_EXPORT(distributed::TxGidPair);
-BOOST_CLASS_EXPORT(distributed::VertexCountReq);
-BOOST_CLASS_EXPORT(distributed::VertexCountRes);
-
-// Distributed plan exchange.
-BOOST_CLASS_EXPORT(distributed::DispatchPlanReq);
-BOOST_CLASS_EXPORT(distributed::DispatchPlanRes);
-BOOST_CLASS_EXPORT(distributed::RemovePlanReq);
-BOOST_CLASS_EXPORT(distributed::RemovePlanRes);
-
-// Pull.
-BOOST_CLASS_EXPORT(distributed::PullReq);
-BOOST_CLASS_EXPORT(distributed::PullRes);
-BOOST_CLASS_EXPORT(distributed::TransactionCommandAdvancedReq);
-BOOST_CLASS_EXPORT(distributed::TransactionCommandAdvancedRes);
-
-// Distributed indexes.
-BOOST_CLASS_EXPORT(distributed::BuildIndexReq);
-BOOST_CLASS_EXPORT(distributed::BuildIndexRes);
-BOOST_CLASS_EXPORT(distributed::IndexLabelPropertyTx);
-
-// Token sharing.
-BOOST_CLASS_EXPORT(distributed::TokenTransferReq);
-BOOST_CLASS_EXPORT(distributed::TokenTransferRes);
-
-// Stats.
-BOOST_CLASS_EXPORT(stats::StatsReq);
-BOOST_CLASS_EXPORT(stats::StatsRes);
-BOOST_CLASS_EXPORT(stats::BatchStatsReq);
-BOOST_CLASS_EXPORT(stats::BatchStatsRes);
-
-// Updates.
-BOOST_CLASS_EXPORT(database::StateDelta);
-BOOST_CLASS_EXPORT(distributed::UpdateReq);
-BOOST_CLASS_EXPORT(distributed::UpdateRes);
-BOOST_CLASS_EXPORT(distributed::UpdateApplyReq);
-BOOST_CLASS_EXPORT(distributed::UpdateApplyRes);
-
-// Creates.
-BOOST_CLASS_EXPORT(distributed::CreateResult);
-BOOST_CLASS_EXPORT(distributed::CreateVertexReq);
-BOOST_CLASS_EXPORT(distributed::CreateVertexReqData);
-BOOST_CLASS_EXPORT(distributed::CreateVertexRes);
-BOOST_CLASS_EXPORT(distributed::CreateEdgeReqData);
-BOOST_CLASS_EXPORT(distributed::CreateEdgeReq);
-BOOST_CLASS_EXPORT(distributed::CreateEdgeRes);
-BOOST_CLASS_EXPORT(distributed::AddInEdgeReqData);
-BOOST_CLASS_EXPORT(distributed::AddInEdgeReq);
-BOOST_CLASS_EXPORT(distributed::AddInEdgeRes);
-
-// Removes.
-BOOST_CLASS_EXPORT(distributed::RemoveVertexReq);
-BOOST_CLASS_EXPORT(distributed::RemoveVertexRes);
-BOOST_CLASS_EXPORT(distributed::RemoveEdgeReq);
-BOOST_CLASS_EXPORT(distributed::RemoveEdgeRes);
-BOOST_CLASS_EXPORT(distributed::RemoveInEdgeData);
-BOOST_CLASS_EXPORT(distributed::RemoveInEdgeReq);
-BOOST_CLASS_EXPORT(distributed::RemoveInEdgeRes);
-
-// Durability
-BOOST_CLASS_EXPORT(distributed::MakeSnapshotReq);
-BOOST_CLASS_EXPORT(distributed::MakeSnapshotRes);
-
-// Storage Gc.
-BOOST_CLASS_EXPORT(distributed::GcClearedStatusReq);
-BOOST_CLASS_EXPORT(distributed::GcClearedStatusRes);
-
-// Transactional Cache Cleaner.
-BOOST_CLASS_EXPORT(distributed::WaitOnTransactionEndReq);
-BOOST_CLASS_EXPORT(distributed::WaitOnTransactionEndRes);
-
-// Cursor.
-BOOST_CLASS_EXPORT(distributed::CreateBfsSubcursorReq);
-BOOST_CLASS_EXPORT(distributed::CreateBfsSubcursorRes);
-BOOST_CLASS_EXPORT(distributed::RegisterSubcursorsReq);
-BOOST_CLASS_EXPORT(distributed::RegisterSubcursorsRes);
-BOOST_CLASS_EXPORT(distributed::RemoveBfsSubcursorReq);
-BOOST_CLASS_EXPORT(distributed::RemoveBfsSubcursorRes);
-BOOST_CLASS_EXPORT(distributed::SetSourceReq);
-BOOST_CLASS_EXPORT(distributed::SetSourceRes);
-BOOST_CLASS_EXPORT(distributed::ExpandLevelReq);
-BOOST_CLASS_EXPORT(distributed::ExpandLevelRes);
-BOOST_CLASS_EXPORT(distributed::SubcursorPullReq);
-BOOST_CLASS_EXPORT(distributed::SubcursorPullRes);
-BOOST_CLASS_EXPORT(distributed::ExpandToRemoteVertexReq);
-BOOST_CLASS_EXPORT(distributed::ExpandToRemoteVertexRes);
-BOOST_CLASS_EXPORT(distributed::ReconstructPathReq);
-BOOST_CLASS_EXPORT(distributed::ReconstructPathRes);
-BOOST_CLASS_EXPORT(distributed::PrepareForExpandReq);
-BOOST_CLASS_EXPORT(distributed::PrepareForExpandRes);
diff --git a/src/communication/rpc/messages.capnp b/src/communication/rpc/messages.capnp
new file mode 100644
index 000000000..507d52148
--- /dev/null
+++ b/src/communication/rpc/messages.capnp
@@ -0,0 +1,9 @@
+@0xd3832c9a1a3d8ec7;
+
+using Cxx = import "/capnp/c++.capnp";
+$Cxx.namespace("communication::rpc::capnp");
+
+struct Message {
+  typeId @0 :UInt64;
+  data @1 :AnyPointer;
+}
diff --git a/src/communication/rpc/messages.hpp b/src/communication/rpc/messages.hpp
index 604868350..982f9896e 100644
--- a/src/communication/rpc/messages.hpp
+++ b/src/communication/rpc/messages.hpp
@@ -1,38 +1,50 @@
 #pragma once
 
+#include <cstdint>
 #include <memory>
-#include <type_traits>
-#include <typeindex>
-
-#include "boost/serialization/access.hpp"
-#include "boost/serialization/base_object.hpp"
 
 namespace communication::rpc {
 
 using MessageSize = uint32_t;
 
-/**
- * Base class for messages.
- */
-class Message {
- public:
-  virtual ~Message() {}
-
-  /**
-   * Run-time type identification that is used for callbacks.
-   *
-   * Warning: this works because of the virtual destructor, don't remove it from
-   * this class
-   */
-  std::type_index type_index() const { return typeid(*this); }
-
- private:
-  friend boost::serialization::access;
-
-  template <class TArchive>
-  void serialize(TArchive &, unsigned int) {}
+/// Type information on a RPC message.
+/// Each message should have a static member `TypeInfo` with this information.
+struct MessageType {
+  /// Unique ID for a message.
+  uint64_t id;
+  /// Pretty name of the type.
+  std::string name;
 };
 
+inline bool operator==(const MessageType &a, const MessageType &b) {
+  return a.id == b.id;
+}
+inline bool operator!=(const MessageType &a, const MessageType &b) {
+  return a.id != b.id;
+}
+inline bool operator<(const MessageType &a, const MessageType &b) {
+  return a.id < b.id;
+}
+inline bool operator<=(const MessageType &a, const MessageType &b) {
+  return a.id <= b.id;
+}
+inline bool operator>(const MessageType &a, const MessageType &b) {
+  return a.id > b.id;
+}
+inline bool operator>=(const MessageType &a, const MessageType &b) {
+  return a.id >= b.id;
+}
+
+/// Each RPC is defined via this struct.
+///
+/// `TRequest` and `TResponse` are required to be classes which have a static
+/// member `TypeInfo` of `MessageType` type. This is used for proper
+/// registration and deserialization of RPC types. Additionally, both `TRequest`
+/// and `TResponse` are required to define a nested `Capnp` type, which
+/// corresponds to the Cap'n Proto schema type, as well as defined the following
+/// serialization functions:
+///   * void Save(Capnp::Builder *, ...) const
+///   * void Load(const Capnp::Reader &, ...)
 template <typename TRequest, typename TResponse>
 struct RequestResponse {
   using Request = TRequest;
@@ -40,35 +52,3 @@ struct RequestResponse {
 };
 
 }  // namespace communication::rpc
-
-// RPC Pimp
-#define RPC_NO_MEMBER_MESSAGE(name)                                       \
-  struct name : public communication::rpc::Message {                      \
-    name() {}                                                             \
-                                                                          \
-   private:                                                               \
-    friend class boost::serialization::access;                            \
-                                                                          \
-    template <class TArchive>                                             \
-    void serialize(TArchive &ar, unsigned int) {                          \
-      ar &boost::serialization::base_object<communication::rpc::Message>( \
-          *this);                                                         \
-    }                                                                     \
-  }
-
-#define RPC_SINGLE_MEMBER_MESSAGE(name, type)                             \
-  struct name : public communication::rpc::Message {                      \
-    name() {}                                                             \
-    name(const type &member) : member(member) {}                          \
-    type member;                                                          \
-                                                                          \
-   private:                                                               \
-    friend class boost::serialization::access;                            \
-                                                                          \
-    template <class TArchive>                                             \
-    void serialize(TArchive &ar, unsigned int) {                          \
-      ar &boost::serialization::base_object<communication::rpc::Message>( \
-          *this);                                                         \
-      ar &member;                                                         \
-    }                                                                     \
-  }
diff --git a/src/communication/rpc/protocol.cpp b/src/communication/rpc/protocol.cpp
index 937532e1e..f05788d49 100644
--- a/src/communication/rpc/protocol.cpp
+++ b/src/communication/rpc/protocol.cpp
@@ -1,11 +1,10 @@
 #include <sstream>
 
-#include "boost/archive/binary_iarchive.hpp"
-#include "boost/archive/binary_oarchive.hpp"
-#include "boost/serialization/unique_ptr.hpp"
+#include "capnp/message.h"
+#include "capnp/serialize.h"
 #include "fmt/format.h"
 
-#include "communication/rpc/messages-inl.hpp"
+#include "communication/rpc/messages.capnp.h"
 #include "communication/rpc/messages.hpp"
 #include "communication/rpc/protocol.hpp"
 #include "communication/rpc/server.hpp"
@@ -28,65 +27,51 @@ void Session::Execute() {
   if (input_stream_.size() < request_size) return;
 
   // Read the request message.
-  std::unique_ptr<Message> request([this, request_len]() {
-    Message *req_ptr = nullptr;
-    std::stringstream stream(std::ios_base::in | std::ios_base::binary);
-    stream.str(std::string(
-        reinterpret_cast<char *>(input_stream_.data() + sizeof(MessageSize)),
-        request_len));
-    boost::archive::binary_iarchive archive(stream);
-    // Sent from client.cpp
-    archive >> req_ptr;
-    return req_ptr;
-  }());
+  auto data =
+      ::kj::arrayPtr(input_stream_.data() + sizeof(request_len), request_len);
+  // Our data is word aligned and padded to 64bit because we use regular
+  // (non-packed) serialization of Cap'n Proto. So we can use reinterpret_cast.
+  auto data_words =
+      ::kj::arrayPtr(reinterpret_cast<::capnp::word *>(data.begin()),
+                     reinterpret_cast<::capnp::word *>(data.end()));
+  ::capnp::FlatArrayMessageReader request_message(data_words.asConst());
+  auto request = request_message.getRoot<capnp::Message>();
   input_stream_.Shift(sizeof(MessageSize) + request_len);
 
   auto callbacks_accessor = server_.callbacks_.access();
-  auto it = callbacks_accessor.find(request->type_index());
+  auto it = callbacks_accessor.find(request.getTypeId());
   if (it == callbacks_accessor.end()) {
     // Throw exception to close the socket and cleanup the session.
     throw SessionException(
         "Session trying to execute an unregistered RPC call!");
   }
 
-  if (VLOG_IS_ON(12)) {
-    auto req_type = utils::Demangle(request->type_index().name());
-    LOG(INFO) << "[RpcServer] received " << (req_type ? req_type.value() : "");
-  }
+  VLOG(12) << "[RpcServer] received " << it->second.req_type.name;
 
-  std::unique_ptr<Message> response = it->second(*(request.get()));
-
-  if (!response) {
-    throw SessionException("Trying to send nullptr instead of message");
-  }
+  ::capnp::MallocMessageBuilder response_message;
+  // callback fills the message data
+  auto response_builder = response_message.initRoot<capnp::Message>();
+  it->second.callback(request, &response_builder);
 
   // Serialize and send response
-  std::stringstream stream(std::ios_base::out | std::ios_base::binary);
-  {
-    boost::archive::binary_oarchive archive(stream);
-    archive << response;
-    // Archive destructor ensures everything is written.
-  }
-
-  const std::string &buffer = stream.str();
-  if (buffer.size() > std::numeric_limits<MessageSize>::max()) {
+  auto response_words = ::capnp::messageToFlatArray(response_message);
+  auto response_bytes = response_words.asBytes();
+  if (response_bytes.size() > std::numeric_limits<MessageSize>::max()) {
     throw SessionException(fmt::format(
         "Trying to send response of size {}, max response size is {}",
-        buffer.size(), std::numeric_limits<MessageSize>::max()));
+        response_bytes.size(), std::numeric_limits<MessageSize>::max()));
   }
 
-  MessageSize input_stream_size = buffer.size();
+  MessageSize input_stream_size = response_bytes.size();
   if (!output_stream_.Write(reinterpret_cast<uint8_t *>(&input_stream_size),
                             sizeof(MessageSize), true)) {
     throw SessionException("Couldn't send response size!");
   }
-  if (!output_stream_.Write(buffer)) {
+  if (!output_stream_.Write(response_bytes.begin(), response_bytes.size())) {
     throw SessionException("Couldn't send response data!");
   }
 
-  if (VLOG_IS_ON(12)) {
-    auto res_type = utils::Demangle(response->type_index().name());
-    LOG(INFO) << "[RpcServer] sent " << (res_type ? res_type.value() : "");
-  }
+  VLOG(12) << "[RpcServer] sent " << it->second.res_type.name;
 }
+
 }  // namespace communication::rpc
diff --git a/src/communication/rpc/server.cpp b/src/communication/rpc/server.cpp
index 19eade72c..5304a698c 100644
--- a/src/communication/rpc/server.cpp
+++ b/src/communication/rpc/server.cpp
@@ -1,10 +1,3 @@
-#include "boost/archive/binary_iarchive.hpp"
-#include "boost/archive/binary_oarchive.hpp"
-#include "boost/serialization/access.hpp"
-#include "boost/serialization/base_object.hpp"
-#include "boost/serialization/export.hpp"
-#include "boost/serialization/unique_ptr.hpp"
-
 #include "communication/rpc/server.hpp"
 
 namespace communication::rpc {
diff --git a/src/communication/rpc/server.hpp b/src/communication/rpc/server.hpp
index 100a0f0eb..a39a25dd3 100644
--- a/src/communication/rpc/server.hpp
+++ b/src/communication/rpc/server.hpp
@@ -1,9 +1,11 @@
 #pragma once
 
-#include <type_traits>
 #include <unordered_map>
 #include <vector>
 
+#include "capnp/any.h"
+
+#include "communication/rpc/messages.capnp.h"
 #include "communication/rpc/messages.hpp"
 #include "communication/rpc/protocol.hpp"
 #include "communication/server.hpp"
@@ -27,57 +29,53 @@ class Server {
 
   const io::network::Endpoint &endpoint() const;
 
-  template <typename TRequestResponse>
-  void Register(
-      std::function<std::unique_ptr<typename TRequestResponse::Response>(
-          const typename TRequestResponse::Request &)>
-          callback) {
-    static_assert(
-        std::is_base_of<Message, typename TRequestResponse::Request>::value,
-        "TRequestResponse::Request must be derived from Message");
-    static_assert(
-        std::is_base_of<Message, typename TRequestResponse::Response>::value,
-        "TRequestResponse::Response must be derived from Message");
+  template <class TRequestResponse>
+  void Register(std::function<
+                void(const typename TRequestResponse::Request::Capnp::Reader &,
+                     typename TRequestResponse::Response::Capnp::Builder *)>
+                    callback) {
+    RpcCallback rpc;
+    rpc.req_type = TRequestResponse::Request::TypeInfo;
+    rpc.res_type = TRequestResponse::Response::TypeInfo;
+    rpc.callback = [callback = callback](const auto &reader, auto *builder) {
+      auto req_data =
+          reader.getData()
+              .template getAs<typename TRequestResponse::Request::Capnp>();
+      builder->setTypeId(TRequestResponse::Response::TypeInfo.id);
+      auto data_builder = builder->initData();
+      auto res_builder =
+          data_builder
+              .template initAs<typename TRequestResponse::Response::Capnp>();
+      callback(req_data, &res_builder);
+    };
     auto callbacks_accessor = callbacks_.access();
-    auto got = callbacks_accessor.insert(
-        typeid(typename TRequestResponse::Request),
-        [callback = callback](const Message &base_message) {
-          const auto &message =
-              dynamic_cast<const typename TRequestResponse::Request &>(
-                  base_message);
-          return callback(message);
-        });
+    auto got =
+        callbacks_accessor.insert(TRequestResponse::Request::TypeInfo.id, rpc);
     CHECK(got.second) << "Callback for that message type already registered";
-    if (VLOG_IS_ON(12)) {
-      auto req_type =
-          utils::Demangle(typeid(typename TRequestResponse::Request).name());
-      auto res_type =
-          utils::Demangle(typeid(typename TRequestResponse::Response).name());
-      LOG(INFO) << "[RpcServer] register " << (req_type ? req_type.value() : "")
-                << " -> " << (res_type ? res_type.value() : "");
-    }
+    VLOG(12) << "[RpcServer] register " << rpc.req_type.name << " -> "
+             << rpc.res_type.name;
   }
 
   template <typename TRequestResponse>
   void UnRegister() {
-    static_assert(
-        std::is_base_of<Message, typename TRequestResponse::Request>::value,
-        "TRequestResponse::Request must be derived from Message");
-    static_assert(
-        std::is_base_of<Message, typename TRequestResponse::Response>::value,
-        "TRequestResponse::Response must be derived from Message");
+    const MessageType &type = TRequestResponse::Request::TypeInfo;
     auto callbacks_accessor = callbacks_.access();
-    auto deleted =
-        callbacks_accessor.remove(typeid(typename TRequestResponse::Request));
+    auto deleted = callbacks_accessor.remove(type.id);
     CHECK(deleted) << "Trying to remove unknown message type callback";
   }
 
  private:
   friend class Session;
 
-  ConcurrentMap<std::type_index,
-                std::function<std::unique_ptr<Message>(const Message &)>>
-      callbacks_;
+  struct RpcCallback {
+    MessageType req_type;
+    std::function<void(const capnp::Message::Reader &,
+                       capnp::Message::Builder *)>
+        callback;
+    MessageType res_type;
+  };
+
+  ConcurrentMap<uint64_t, RpcCallback> callbacks_;
 
   std::mutex mutex_;
   communication::Server<Session, Server> server_;
diff --git a/src/database/counters.cpp b/src/database/counters.cpp
index 5ee380ddc..97814b31d 100644
--- a/src/database/counters.cpp
+++ b/src/database/counters.cpp
@@ -1,23 +1,9 @@
 #include "database/counters.hpp"
 
-#include "boost/archive/binary_iarchive.hpp"
-#include "boost/archive/binary_oarchive.hpp"
-#include "boost/serialization/export.hpp"
-#include "boost/serialization/utility.hpp"
+#include "database/counters_rpc_messages.hpp"
 
 namespace database {
 
-RPC_SINGLE_MEMBER_MESSAGE(CountersGetReq, std::string);
-RPC_SINGLE_MEMBER_MESSAGE(CountersGetRes, int64_t);
-using CountersGetRpc =
-    communication::rpc::RequestResponse<CountersGetReq, CountersGetRes>;
-
-using CountersSetReqData = std::pair<std::string, int64_t>;
-RPC_SINGLE_MEMBER_MESSAGE(CountersSetReq, CountersSetReqData);
-RPC_NO_MEMBER_MESSAGE(CountersSetRes);
-using CountersSetRpc =
-    communication::rpc::RequestResponse<CountersSetReq, CountersSetRes>;
-
 int64_t SingleNodeCounters::Get(const std::string &name) {
   return counters_.access()
       .emplace(name, std::make_tuple(name), std::make_tuple(0))
@@ -32,13 +18,16 @@ void SingleNodeCounters::Set(const std::string &name, int64_t value) {
 
 MasterCounters::MasterCounters(communication::rpc::Server &server)
     : rpc_server_(server) {
-  rpc_server_.Register<CountersGetRpc>([this](const CountersGetReq &req) {
-    return std::make_unique<CountersGetRes>(Get(req.member));
-  });
-  rpc_server_.Register<CountersSetRpc>([this](const CountersSetReq &req) {
-    Set(req.member.first, req.member.second);
-    return std::make_unique<CountersSetRes>();
-  });
+  rpc_server_.Register<CountersGetRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        CountersGetRes res(Get(req_reader.getName()));
+        res.Save(res_builder);
+      });
+  rpc_server_.Register<CountersSetRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        Set(req_reader.getName(), req_reader.getValue());
+        return std::make_unique<CountersSetRes>();
+      });
 }
 
 WorkerCounters::WorkerCounters(
@@ -48,18 +37,12 @@ WorkerCounters::WorkerCounters(
 int64_t WorkerCounters::Get(const std::string &name) {
   auto response = master_client_pool_.Call<CountersGetRpc>(name);
   CHECK(response) << "CountersGetRpc failed";
-  return response->member;
+  return response->value;
 }
 
 void WorkerCounters::Set(const std::string &name, int64_t value) {
-  auto response =
-      master_client_pool_.Call<CountersSetRpc>(CountersSetReqData{name, value});
+  auto response = master_client_pool_.Call<CountersSetRpc>(name, value);
   CHECK(response) << "CountersSetRpc failed";
 }
 
 }  // namespace database
-
-BOOST_CLASS_EXPORT(database::CountersGetReq);
-BOOST_CLASS_EXPORT(database::CountersGetRes);
-BOOST_CLASS_EXPORT(database::CountersSetReq);
-BOOST_CLASS_EXPORT(database::CountersSetRes);
diff --git a/src/database/counters.hpp b/src/database/counters.hpp
index c5661107b..1da76d7bd 100644
--- a/src/database/counters.hpp
+++ b/src/database/counters.hpp
@@ -5,7 +5,6 @@
 #include <string>
 
 #include "communication/rpc/client_pool.hpp"
-#include "communication/rpc/messages.hpp"
 #include "communication/rpc/server.hpp"
 #include "data_structures/concurrent/concurrent_map.hpp"
 
@@ -45,7 +44,7 @@ class SingleNodeCounters : public Counters {
 /** Implementation for distributed master. */
 class MasterCounters : public SingleNodeCounters {
  public:
-  MasterCounters(communication::rpc::Server &server);
+  explicit MasterCounters(communication::rpc::Server &server);
 
  private:
   communication::rpc::Server &rpc_server_;
@@ -54,7 +53,7 @@ class MasterCounters : public SingleNodeCounters {
 /** Implementation for distributed worker. */
 class WorkerCounters : public Counters {
  public:
-  WorkerCounters(communication::rpc::ClientPool &master_client_pool);
+  explicit WorkerCounters(communication::rpc::ClientPool &master_client_pool);
 
   int64_t Get(const std::string &name) override;
   void Set(const std::string &name, int64_t value) override;
diff --git a/src/database/counters_rpc_messages.lcp b/src/database/counters_rpc_messages.lcp
new file mode 100644
index 000000000..9b1834b83
--- /dev/null
+++ b/src/database/counters_rpc_messages.lcp
@@ -0,0 +1,23 @@
+#>cpp
+#pragma once
+
+#include <string>
+
+#include "communication/rpc/messages.hpp"
+#include "database/counters_rpc_messages.capnp.h"
+cpp<#
+
+(lcp:namespace database)
+
+(lcp:capnp-namespace "database")
+
+(lcp:define-rpc counters-get
+    (:request ((name "std::string")))
+  (:response ((value :int64_t))))
+
+(lcp:define-rpc counters-set
+    (:request ((name "std::string")
+               (value :int64_t)))
+  (:response ()))
+
+(lcp:pop-namespace) ;; database
diff --git a/src/database/state_delta.hpp b/src/database/state_delta.hpp
deleted file mode 100644
index 7c87e9c58..000000000
--- a/src/database/state_delta.hpp
+++ /dev/null
@@ -1,183 +0,0 @@
-#pragma once
-
-#include "communication/bolt/v1/decoder/decoder.hpp"
-#include "communication/bolt/v1/encoder/primitive_encoder.hpp"
-#include "durability/hashed_file_reader.hpp"
-#include "durability/hashed_file_writer.hpp"
-#include "storage/address_types.hpp"
-#include "storage/gid.hpp"
-#include "storage/property_value.hpp"
-#include "utils/serialization.hpp"
-
-namespace database {
-/** Describes single change to the database state. Used for durability (WAL) and
- * state communication over network in HA and for distributed remote storage
- * changes.
- *
- * Labels, Properties and EdgeTypes are stored both as values (integers) and
- * strings (their names). The values are used when applying deltas in a running
- * database. Names are used when recovering the database as it's not guaranteed
- * that after recovery the old name<->value mapping will be preserved.
- *
- * TODO: ensure the mapping is preserved after recovery and don't save strings
- * in StateDeltas. */
-struct StateDelta {
-  /** Defines StateDelta type. For each type the comment indicates which values
-   * need to be stored. All deltas have the transaction_id member, so that's
-   * omitted in the comment. */
-  enum class Type {
-    TRANSACTION_BEGIN,
-    TRANSACTION_COMMIT,
-    TRANSACTION_ABORT,
-    CREATE_VERTEX,    // vertex_id
-    CREATE_EDGE,      // edge_id, from_vertex_id, to_vertex_id, edge_type,
-                      // edge_type_name
-    ADD_OUT_EDGE,     // vertex_id, edge_address, vertex_to_address, edge_type
-    REMOVE_OUT_EDGE,  // vertex_id, edge_address
-    ADD_IN_EDGE,      // vertex_id, edge_address, vertex_from_address, edge_type
-    REMOVE_IN_EDGE,   // vertex_id, edge_address
-    SET_PROPERTY_VERTEX,  // vertex_id, property, property_name, property_value
-    SET_PROPERTY_EDGE,    // edge_id, property, property_name, property_value
-    // remove property is done by setting a PropertyValue::Null
-    ADD_LABEL,      // vertex_id, label, label_name
-    REMOVE_LABEL,   // vertex_id, label, label_name
-    REMOVE_VERTEX,  // vertex_id, check_empty
-    REMOVE_EDGE,    // edge_id
-    BUILD_INDEX     // label, label_name, property, property_name
-  };
-
-  StateDelta() = default;
-  StateDelta(const enum Type &type, tx::TransactionId tx_id)
-      : type(type), transaction_id(tx_id) {}
-
-  /** Attempts to decode a StateDelta from the given decoder. Returns the
-   * decoded value if successful, otherwise returns nullopt. */
-  static std::experimental::optional<StateDelta> Decode(
-      HashedFileReader &reader,
-      communication::bolt::Decoder<HashedFileReader> &decoder);
-
-  /** Encodes the delta using primitive encoder, and writes out the new hash
-   * with delta to the writer */
-  void Encode(
-      HashedFileWriter &writer,
-      communication::bolt::PrimitiveEncoder<HashedFileWriter> &encoder) const;
-
-  static StateDelta TxBegin(tx::TransactionId tx_id);
-  static StateDelta TxCommit(tx::TransactionId tx_id);
-  static StateDelta TxAbort(tx::TransactionId tx_id);
-  static StateDelta CreateVertex(tx::TransactionId tx_id,
-                                 gid::Gid vertex_id);
-  static StateDelta CreateEdge(tx::TransactionId tx_id, gid::Gid edge_id,
-                               gid::Gid vertex_from_id, gid::Gid vertex_to_id,
-                               storage::EdgeType edge_type,
-                               const std::string &edge_type_name);
-  static StateDelta AddOutEdge(tx::TransactionId tx_id, gid::Gid vertex_id,
-                               storage::VertexAddress vertex_to_address,
-                               storage::EdgeAddress edge_address,
-                               storage::EdgeType edge_type);
-  static StateDelta RemoveOutEdge(tx::TransactionId tx_id,
-                                  gid::Gid vertex_id,
-                                  storage::EdgeAddress edge_address);
-  static StateDelta AddInEdge(tx::TransactionId tx_id, gid::Gid vertex_id,
-                              storage::VertexAddress vertex_from_address,
-                              storage::EdgeAddress edge_address,
-                              storage::EdgeType edge_type);
-  static StateDelta RemoveInEdge(tx::TransactionId tx_id, gid::Gid vertex_id,
-                                 storage::EdgeAddress edge_address);
-  static StateDelta PropsSetVertex(tx::TransactionId tx_id,
-                                   gid::Gid vertex_id,
-                                   storage::Property property,
-                                   const std::string &property_name,
-                                   const PropertyValue &value);
-  static StateDelta PropsSetEdge(tx::TransactionId tx_id, gid::Gid edge_id,
-                                 storage::Property property,
-                                 const std::string &property_name,
-                                 const PropertyValue &value);
-  static StateDelta AddLabel(tx::TransactionId tx_id, gid::Gid vertex_id,
-                             storage::Label label,
-                             const std::string &label_name);
-  static StateDelta RemoveLabel(tx::TransactionId tx_id, gid::Gid vertex_id,
-                                storage::Label label,
-                                const std::string &label_name);
-  static StateDelta RemoveVertex(tx::TransactionId tx_id, gid::Gid vertex_id,
-                                 bool check_empty);
-  static StateDelta RemoveEdge(tx::TransactionId tx_id, gid::Gid edge_id);
-  static StateDelta BuildIndex(tx::TransactionId tx_id, storage::Label label,
-                               const std::string &label_name,
-                               storage::Property property,
-                               const std::string &property_name);
-
-  /// Applies CRUD delta to database accessor. Fails on other types of deltas
-  void Apply(GraphDbAccessor &dba) const;
-
-  // Members valid for every delta.
-  enum Type type;
-  tx::TransactionId transaction_id;
-
-  // Members valid only for some deltas, see StateDelta::Type comments above.
-  // TODO: when preparing the WAL for distributed, most likely remove Gids and
-  // only keep addresses.
-  gid::Gid vertex_id;
-  gid::Gid edge_id;
-  storage::EdgeAddress edge_address;
-  gid::Gid vertex_from_id;
-  storage::VertexAddress vertex_from_address;
-  gid::Gid vertex_to_id;
-  storage::VertexAddress vertex_to_address;
-  storage::EdgeType edge_type;
-  std::string edge_type_name;
-  storage::Property property;
-  std::string property_name;
-  PropertyValue value = PropertyValue::Null;
-  storage::Label label;
-  std::string label_name;
-  bool check_empty;
-
- private:
-  friend class boost::serialization::access;
-  BOOST_SERIALIZATION_SPLIT_MEMBER();
-  template <class TArchive>
-  void save(TArchive &ar, const unsigned int) const {
-    ar &type;
-    ar &transaction_id;
-    ar &vertex_id;
-    ar &edge_id;
-    ar &edge_address;
-    ar &vertex_from_id;
-    ar &vertex_from_address;
-    ar &vertex_to_id;
-    ar &vertex_to_address;
-    ar &edge_type;
-    ar &edge_type_name;
-    ar &property;
-    ar &property_name;
-    utils::SaveTypedValue(ar, value);
-    ar &label;
-    ar &label_name;
-    ar &check_empty;
-  }
-
-  template <class TArchive>
-  void load(TArchive &ar, const unsigned int) {
-    ar &type;
-    ar &transaction_id;
-    ar &vertex_id;
-    ar &edge_id;
-    ar &edge_address;
-    ar &vertex_from_id;
-    ar &vertex_from_address;
-    ar &vertex_to_id;
-    ar &vertex_to_address;
-    ar &edge_type;
-    ar &edge_type_name;
-    ar &property;
-    ar &property_name;
-    query::TypedValue tv;
-    utils::LoadTypedValue(ar, tv);
-    value = tv;
-    ar &label;
-    ar &label_name;
-    ar &check_empty;
-  }
-};
-}  // namespace database
diff --git a/src/database/state_delta.lcp b/src/database/state_delta.lcp
new file mode 100644
index 000000000..f395f4c3c
--- /dev/null
+++ b/src/database/state_delta.lcp
@@ -0,0 +1,179 @@
+#>cpp
+#pragma once
+
+#include "communication/bolt/v1/decoder/decoder.hpp"
+#include "communication/bolt/v1/encoder/primitive_encoder.hpp"
+#include "database/state_delta.capnp.h"
+#include "durability/hashed_file_reader.hpp"
+#include "durability/hashed_file_writer.hpp"
+#include "storage/address_types.hpp"
+#include "storage/gid.hpp"
+#include "storage/property_value.hpp"
+#include "utils/serialization.hpp"
+cpp<#
+
+(lcp:namespace database)
+
+(lcp:capnp-namespace "database")
+
+(lcp:capnp-import 'storage "/storage/serialization.capnp")
+(lcp:capnp-import 'dis "/distributed/serialization.capnp")
+
+(lcp:capnp-type-conversion "tx::TransactionId" "UInt64")
+(lcp:capnp-type-conversion "gid::Gid" "UInt64")
+(lcp:capnp-type-conversion "storage::Label" "Storage.Common")
+(lcp:capnp-type-conversion "storage::EdgeType" "Storage.Common")
+(lcp:capnp-type-conversion "storage::Property" "Storage.Common")
+(lcp:capnp-type-conversion "storage::EdgeAddress" "Storage.Address")
+(lcp:capnp-type-conversion "storage::VertexAddress" "Storage.Address")
+
+(lcp:define-struct state-delta ()
+  (
+   ;; Members valid for every delta.
+   (type "Type" :capnp-init nil
+         :capnp-save (lcp:capnp-save-enum "capnp::StateDelta::Type" "Type")
+         :capnp-load (lcp:capnp-load-enum "capnp::StateDelta::Type" "Type"))
+   (transaction-id "tx::TransactionId")
+   ;; Members valid only for some deltas, see StateDelta::Type comments above.
+   ;; TODO: when preparing the WAL for distributed, most likely remove Gids and
+   ;; only keep addresses.
+   (vertex-id "gid::Gid")
+   (edge-id "gid::Gid")
+   (edge-address "storage::EdgeAddress")
+   (vertex-from-id "gid::Gid")
+   (vertex-from-address "storage::VertexAddress")
+   (vertex-to-id "gid::Gid")
+   (vertex-to-address "storage::VertexAddress")
+   (edge-type "storage::EdgeType")
+   (edge-type-name "std::string")
+   (property "storage::Property")
+   (property-name "std::string")
+   (value "PropertyValue" :initval "PropertyValue::Null"
+          :save-fun #>cpp utils::SaveTypedValue(ar, value); cpp<#
+          :load-fun
+          #>cpp
+          query::TypedValue tv;
+          utils::LoadTypedValue(ar, tv);
+          value = tv;
+          cpp<#
+          :capnp-type "Dis.TypedValue"
+          :capnp-save
+          (lambda (builder member)
+            #>cpp
+            utils::SaveCapnpTypedValue(${member}, &${builder});
+            cpp<#)
+          :capnp-load
+          (lambda (reader member)
+            #>cpp
+            query::TypedValue tv;
+            utils::LoadCapnpTypedValue(${reader}, &tv);
+            ${member} = tv;
+            cpp<#))
+   (label "storage::Label")
+   (label-name "std::string")
+   (check-empty :bool))
+  (:documentation
+   "Describes single change to the database state. Used for durability (WAL) and
+state communication over network in HA and for distributed remote storage
+changes.
+
+Labels, Properties and EdgeTypes are stored both as values (integers) and
+strings (their names). The values are used when applying deltas in a running
+database. Names are used when recovering the database as it's not guaranteed
+that after recovery the old name<->value mapping will be preserved.
+
+TODO: ensure the mapping is preserved after recovery and don't save strings
+in StateDeltas.")
+  (:public
+   (lcp:define-enum type
+       (transaction-begin
+        transaction-commit
+        transaction-abort
+        create-vertex ;; vertex_id
+        create-edge   ;; edge_id, from_vertex_id, to_vertex_id, edge_type, edge_type_name
+        add-out-edge  ;; vertex_id, edge_address, vertex_to_address, edge_type
+        remove-out-edge ;; vertex_id, edge_address
+        add-in-edge     ;; vertex_id, edge_address, vertex_from_address, edge_type
+        remove-in-edge  ;; vertex_id, edge_address
+        set-property-vertex ;; vertex_id, property, property_name, property_value
+        set-property-edge   ;; edge_id, property, property_name, property_value
+        ;; remove property is done by setting a PropertyValue::Null
+        add-label     ;; vertex_id, label, label_name
+        remove-label  ;; vertex_id, label, label_name
+        remove-vertex ;; vertex_id, check_empty
+        remove-edge   ;; edge_id
+        build-index   ;; label, label_name, property, property_name
+        )
+     (:documentation
+      "Defines StateDelta type. For each type the comment indicates which values
+need to be stored. All deltas have the transaction_id member, so that's
+omitted in the comment.")
+     (:serialize :capnp))
+     #>cpp
+     StateDelta() = default;
+     StateDelta(const enum Type &type, tx::TransactionId tx_id)
+         : type(type), transaction_id(tx_id) {}
+
+     /** Attempts to decode a StateDelta from the given decoder. Returns the
+      * decoded value if successful, otherwise returns nullopt. */
+     static std::experimental::optional<StateDelta> Decode(
+         HashedFileReader &reader,
+         communication::bolt::Decoder<HashedFileReader> &decoder);
+
+     /** Encodes the delta using primitive encoder, and writes out the new hash
+      * with delta to the writer */
+     void Encode(
+         HashedFileWriter &writer,
+         communication::bolt::PrimitiveEncoder<HashedFileWriter> &encoder) const;
+
+     static StateDelta TxBegin(tx::TransactionId tx_id);
+     static StateDelta TxCommit(tx::TransactionId tx_id);
+     static StateDelta TxAbort(tx::TransactionId tx_id);
+     static StateDelta CreateVertex(tx::TransactionId tx_id,
+                                    gid::Gid vertex_id);
+     static StateDelta CreateEdge(tx::TransactionId tx_id, gid::Gid edge_id,
+                                  gid::Gid vertex_from_id, gid::Gid vertex_to_id,
+                                  storage::EdgeType edge_type,
+                                  const std::string &edge_type_name);
+     static StateDelta AddOutEdge(tx::TransactionId tx_id, gid::Gid vertex_id,
+                                  storage::VertexAddress vertex_to_address,
+                                  storage::EdgeAddress edge_address,
+                                  storage::EdgeType edge_type);
+     static StateDelta RemoveOutEdge(tx::TransactionId tx_id,
+                                     gid::Gid vertex_id,
+                                     storage::EdgeAddress edge_address);
+     static StateDelta AddInEdge(tx::TransactionId tx_id, gid::Gid vertex_id,
+                                 storage::VertexAddress vertex_from_address,
+                                 storage::EdgeAddress edge_address,
+                                 storage::EdgeType edge_type);
+     static StateDelta RemoveInEdge(tx::TransactionId tx_id, gid::Gid vertex_id,
+                                    storage::EdgeAddress edge_address);
+     static StateDelta PropsSetVertex(tx::TransactionId tx_id,
+                                      gid::Gid vertex_id,
+                                      storage::Property property,
+                                      const std::string &property_name,
+                                      const PropertyValue &value);
+     static StateDelta PropsSetEdge(tx::TransactionId tx_id, gid::Gid edge_id,
+                                    storage::Property property,
+                                    const std::string &property_name,
+                                    const PropertyValue &value);
+     static StateDelta AddLabel(tx::TransactionId tx_id, gid::Gid vertex_id,
+                                storage::Label label,
+                                const std::string &label_name);
+     static StateDelta RemoveLabel(tx::TransactionId tx_id, gid::Gid vertex_id,
+                                   storage::Label label,
+                                   const std::string &label_name);
+     static StateDelta RemoveVertex(tx::TransactionId tx_id, gid::Gid vertex_id,
+                                    bool check_empty);
+     static StateDelta RemoveEdge(tx::TransactionId tx_id, gid::Gid edge_id);
+     static StateDelta BuildIndex(tx::TransactionId tx_id, storage::Label label,
+                                  const std::string &label_name,
+                                  storage::Property property,
+                                  const std::string &property_name);
+
+     /// Applies CRUD delta to database accessor. Fails on other types of deltas
+     void Apply(GraphDbAccessor &dba) const;
+     cpp<#)
+  (:serialize :capnp))
+
+(lcp:pop-namespace) ;; database
diff --git a/src/database/storage_gc_master.hpp b/src/database/storage_gc_master.hpp
index 02b9d513b..81d5635ac 100644
--- a/src/database/storage_gc_master.hpp
+++ b/src/database/storage_gc_master.hpp
@@ -17,10 +17,11 @@ class StorageGcMaster : public StorageGc {
         rpc_server_(rpc_server),
         coordination_(coordination) {
     rpc_server_.Register<distributed::RanLocalGcRpc>(
-        [this](const distributed::GcClearedStatusReq &req) {
+        [this](const auto &req_reader, auto *res_builder) {
+          distributed::RanLocalGcReq req;
+          req.Load(req_reader);
           std::unique_lock<std::mutex> lock(worker_safe_transaction_mutex_);
           worker_safe_transaction_[req.worker_id] = req.local_oldest_active;
-          return std::make_unique<distributed::GcClearedStatusRes>();
         });
   }
 
diff --git a/src/distributed/bfs_rpc_clients.cpp b/src/distributed/bfs_rpc_clients.cpp
index b6ccdb98f..c0a29d9eb 100644
--- a/src/distributed/bfs_rpc_clients.cpp
+++ b/src/distributed/bfs_rpc_clients.cpp
@@ -10,11 +10,11 @@ BfsRpcClients::BfsRpcClients(
     distributed::RpcWorkerClients *clients)
     : db_(db), subcursor_storage_(subcursor_storage), clients_(clients) {}
 
-std::unordered_map<int, int64_t> BfsRpcClients::CreateBfsSubcursors(
+std::unordered_map<int16_t, int64_t> BfsRpcClients::CreateBfsSubcursors(
     tx::TransactionId tx_id, query::EdgeAtom::Direction direction,
     const std::vector<storage::EdgeType> &edge_types,
     query::GraphView graph_view) {
-  auto futures = clients_->ExecuteOnWorkers<std::pair<int, int64_t>>(
+  auto futures = clients_->ExecuteOnWorkers<std::pair<int16_t, int64_t>>(
       db_->WorkerId(),
       [tx_id, direction, &edge_types, graph_view](int worker_id, auto &client) {
         auto res = client.template Call<CreateBfsSubcursorRpc>(
@@ -22,7 +22,7 @@ std::unordered_map<int, int64_t> BfsRpcClients::CreateBfsSubcursors(
         CHECK(res) << "CreateBfsSubcursor RPC failed!";
         return std::make_pair(worker_id, res->member);
       });
-  std::unordered_map<int, int64_t> subcursor_ids;
+  std::unordered_map<int16_t, int64_t> subcursor_ids;
   subcursor_ids.emplace(
       db_->WorkerId(),
       subcursor_storage_->Create(tx_id, direction, edge_types, graph_view));
@@ -34,7 +34,7 @@ std::unordered_map<int, int64_t> BfsRpcClients::CreateBfsSubcursors(
 }
 
 void BfsRpcClients::RegisterSubcursors(
-    const std::unordered_map<int, int64_t> &subcursor_ids) {
+    const std::unordered_map<int16_t, int64_t> &subcursor_ids) {
   auto futures = clients_->ExecuteOnWorkers<void>(
       db_->WorkerId(), [&subcursor_ids](int worker_id, auto &client) {
         auto res = client.template Call<RegisterSubcursorsRpc>(subcursor_ids);
@@ -45,7 +45,7 @@ void BfsRpcClients::RegisterSubcursors(
 }
 
 void BfsRpcClients::RemoveBfsSubcursors(
-    const std::unordered_map<int, int64_t> &subcursor_ids) {
+    const std::unordered_map<int16_t, int64_t> &subcursor_ids) {
   auto futures = clients_->ExecuteOnWorkers<void>(
       db_->WorkerId(), [&subcursor_ids](int worker_id, auto &client) {
         auto res = client.template Call<RemoveBfsSubcursorRpc>(
@@ -56,7 +56,7 @@ void BfsRpcClients::RemoveBfsSubcursors(
 }
 
 std::experimental::optional<VertexAccessor> BfsRpcClients::Pull(
-    int worker_id, int64_t subcursor_id, database::GraphDbAccessor *dba) {
+    int16_t worker_id, int64_t subcursor_id, database::GraphDbAccessor *dba) {
   if (worker_id == db_->WorkerId()) {
     return subcursor_storage_->Get(subcursor_id)->Pull();
   }
@@ -75,7 +75,7 @@ std::experimental::optional<VertexAccessor> BfsRpcClients::Pull(
 }
 
 bool BfsRpcClients::ExpandLevel(
-    const std::unordered_map<int, int64_t> &subcursor_ids) {
+    const std::unordered_map<int16_t, int64_t> &subcursor_ids) {
   auto futures = clients_->ExecuteOnWorkers<bool>(
       db_->WorkerId(), [&subcursor_ids](int worker_id, auto &client) {
         auto res =
@@ -92,7 +92,7 @@ bool BfsRpcClients::ExpandLevel(
 }
 
 void BfsRpcClients::SetSource(
-    const std::unordered_map<int, int64_t> &subcursor_ids,
+    const std::unordered_map<int16_t, int64_t> &subcursor_ids,
     storage::VertexAddress source_address) {
   CHECK(source_address.is_remote())
       << "SetSource should be called with global address";
@@ -109,8 +109,8 @@ void BfsRpcClients::SetSource(
 }
 
 bool BfsRpcClients::ExpandToRemoteVertex(
-    const std::unordered_map<int, int64_t> &subcursor_ids, EdgeAccessor edge,
-    VertexAccessor vertex) {
+    const std::unordered_map<int16_t, int64_t> &subcursor_ids,
+    EdgeAccessor edge, VertexAccessor vertex) {
   CHECK(!vertex.is_local())
       << "ExpandToRemoteVertex should not be called with local vertex";
   int worker_id = vertex.address().worker_id();
@@ -137,7 +137,7 @@ PathSegment BuildPathSegment(ReconstructPathRes *res,
 }
 
 PathSegment BfsRpcClients::ReconstructPath(
-    const std::unordered_map<int, int64_t> &subcursor_ids,
+    const std::unordered_map<int16_t, int64_t> &subcursor_ids,
     storage::VertexAddress vertex, database::GraphDbAccessor *dba) {
   int worker_id = vertex.worker_id();
   if (worker_id == db_->WorkerId()) {
@@ -147,11 +147,11 @@ PathSegment BfsRpcClients::ReconstructPath(
 
   auto res = clients_->GetClientPool(worker_id).Call<ReconstructPathRpc>(
       subcursor_ids.at(worker_id), vertex);
-  return BuildPathSegment(res.get(), dba);
+  return BuildPathSegment(&res.value(), dba);
 }
 
 PathSegment BfsRpcClients::ReconstructPath(
-    const std::unordered_map<int, int64_t> &subcursor_ids,
+    const std::unordered_map<int16_t, int64_t> &subcursor_ids,
     storage::EdgeAddress edge, database::GraphDbAccessor *dba) {
   int worker_id = edge.worker_id();
   if (worker_id == db_->WorkerId()) {
@@ -160,11 +160,11 @@ PathSegment BfsRpcClients::ReconstructPath(
   }
   auto res = clients_->GetClientPool(worker_id).Call<ReconstructPathRpc>(
       subcursor_ids.at(worker_id), edge);
-  return BuildPathSegment(res.get(), dba);
+  return BuildPathSegment(&res.value(), dba);
 }
 
 void BfsRpcClients::PrepareForExpand(
-    const std::unordered_map<int, int64_t> &subcursor_ids, bool clear) {
+    const std::unordered_map<int16_t, int64_t> &subcursor_ids, bool clear) {
   auto res = clients_->ExecuteOnWorkers<void>(
       db_->WorkerId(), [clear, &subcursor_ids](int worker_id, auto &client) {
         auto res = client.template Call<PrepareForExpandRpc>(
diff --git a/src/distributed/bfs_rpc_clients.hpp b/src/distributed/bfs_rpc_clients.hpp
index 41cce6cd1..a60acdf29 100644
--- a/src/distributed/bfs_rpc_clients.hpp
+++ b/src/distributed/bfs_rpc_clients.hpp
@@ -19,39 +19,39 @@ class BfsRpcClients {
                 distributed::BfsSubcursorStorage *subcursor_storage,
                 distributed::RpcWorkerClients *clients);
 
-  std::unordered_map<int, int64_t> CreateBfsSubcursors(
+  std::unordered_map<int16_t, int64_t> CreateBfsSubcursors(
       tx::TransactionId tx_id, query::EdgeAtom::Direction direction,
       const std::vector<storage::EdgeType> &edge_types,
       query::GraphView graph_view);
 
   void RegisterSubcursors(
-      const std::unordered_map<int, int64_t> &subcursor_ids);
+      const std::unordered_map<int16_t, int64_t> &subcursor_ids);
 
   void RemoveBfsSubcursors(
-      const std::unordered_map<int, int64_t> &subcursor_ids);
+      const std::unordered_map<int16_t, int64_t> &subcursor_ids);
 
   std::experimental::optional<VertexAccessor> Pull(
-      int worker_id, int64_t subcursor_id, database::GraphDbAccessor *dba);
+      int16_t worker_id, int64_t subcursor_id, database::GraphDbAccessor *dba);
 
-  bool ExpandLevel(const std::unordered_map<int, int64_t> &subcursor_ids);
+  bool ExpandLevel(const std::unordered_map<int16_t, int64_t> &subcursor_ids);
 
-  void SetSource(const std::unordered_map<int, int64_t> &subcursor_ids,
+  void SetSource(const std::unordered_map<int16_t, int64_t> &subcursor_ids,
                  storage::VertexAddress source_address);
 
   bool ExpandToRemoteVertex(
-      const std::unordered_map<int, int64_t> &subcursor_ids, EdgeAccessor edge,
-      VertexAccessor vertex);
+      const std::unordered_map<int16_t, int64_t> &subcursor_ids,
+      EdgeAccessor edge, VertexAccessor vertex);
 
   PathSegment ReconstructPath(
-      const std::unordered_map<int, int64_t> &subcursor_ids,
+      const std::unordered_map<int16_t, int64_t> &subcursor_ids,
       storage::EdgeAddress edge, database::GraphDbAccessor *dba);
 
   PathSegment ReconstructPath(
-      const std::unordered_map<int, int64_t> &subcursor_ids,
+      const std::unordered_map<int16_t, int64_t> &subcursor_ids,
       storage::VertexAddress vertex, database::GraphDbAccessor *dba);
 
-  void PrepareForExpand(const std::unordered_map<int, int64_t> &subcursor_ids,
-                        bool clear);
+  void PrepareForExpand(
+      const std::unordered_map<int16_t, int64_t> &subcursor_ids, bool clear);
 
  private:
   database::GraphDb *db_;
diff --git a/src/distributed/bfs_rpc_messages.hpp b/src/distributed/bfs_rpc_messages.hpp
deleted file mode 100644
index 23f0e6d94..000000000
--- a/src/distributed/bfs_rpc_messages.hpp
+++ /dev/null
@@ -1,319 +0,0 @@
-#pragma once
-
-#include <tuple>
-
-#include "communication/rpc/messages.hpp"
-#include "distributed/bfs_subcursor.hpp"
-#include "query/plan/operator.hpp"
-#include "transactions/type.hpp"
-#include "utils/serialization.hpp"
-
-namespace distributed {
-
-template <class TElement>
-struct SerializedGraphElement {
-  using AddressT = storage::Address<mvcc::VersionList<TElement>>;
-  using AccessorT = RecordAccessor<TElement>;
-
-  SerializedGraphElement(AddressT global_address, TElement *old_element_input,
-                         TElement *new_element_input, int worker_id)
-      : global_address(global_address),
-        old_element_input(old_element_input),
-        old_element_output(nullptr),
-        new_element_input(new_element_input),
-        new_element_output(nullptr),
-        worker_id(worker_id) {
-    CHECK(global_address.is_remote())
-        << "Only global addresses should be used with SerializedGraphElement";
-  }
-
-  SerializedGraphElement(const AccessorT &accessor)
-      : SerializedGraphElement(accessor.GlobalAddress(), accessor.GetOld(),
-                               accessor.GetNew(),
-                               accessor.db_accessor().db().WorkerId()) {}
-
-  SerializedGraphElement() {}
-
-  AddressT global_address;
-  TElement *old_element_input;
-  std::unique_ptr<TElement> old_element_output;
-  TElement *new_element_input;
-  std::unique_ptr<TElement> new_element_output;
-  int worker_id;
-
- private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void save(TArchive &ar, unsigned int) const {
-    ar << global_address;
-    if (old_element_input) {
-      ar << true;
-      SaveElement(ar, *old_element_input, worker_id);
-    } else {
-      ar << false;
-    }
-    if (new_element_input) {
-      ar << true;
-      SaveElement(ar, *new_element_input, worker_id);
-    } else {
-      ar << false;
-    }
-  }
-
-  template <class TArchive>
-  void load(TArchive &ar, unsigned int) {
-    ar >> global_address;
-    static_assert(std::is_same<TElement, Vertex>::value ||
-                      std::is_same<TElement, Edge>::value,
-                  "TElement should be either Vertex or Edge");
-    bool has_old;
-    ar >> has_old;
-    if (has_old) {
-      if constexpr (std::is_same<TElement, Vertex>::value) {
-        old_element_output = std::move(LoadVertex(ar));
-      } else {
-        old_element_output = std::move(LoadEdge(ar));
-      }
-    }
-    bool has_new;
-    ar >> has_new;
-    if (has_new) {
-      if constexpr (std::is_same<TElement, Vertex>::value) {
-        new_element_output = std::move(LoadVertex(ar));
-      } else {
-        new_element_output = std::move(LoadEdge(ar));
-      }
-    }
-  }
-
-  BOOST_SERIALIZATION_SPLIT_MEMBER()
-};  // namespace distributed
-
-using SerializedVertex = SerializedGraphElement<Vertex>;
-using SerializedEdge = SerializedGraphElement<Edge>;
-
-struct CreateBfsSubcursorReq : public communication::rpc::Message {
-  tx::TransactionId tx_id;
-  query::EdgeAtom::Direction direction;
-  std::vector<storage::EdgeType> edge_types;
-  query::GraphView graph_view;
-
-  CreateBfsSubcursorReq(tx::TransactionId tx_id,
-                        query::EdgeAtom::Direction direction,
-                        std::vector<storage::EdgeType> edge_types,
-                        query::GraphView graph_view)
-      : tx_id(tx_id),
-        direction(direction),
-        edge_types(std::move(edge_types)),
-        graph_view(graph_view) {}
-
- private:
-  friend class boost::serialization::access;
-
-  CreateBfsSubcursorReq() {}
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<communication::rpc::Message>(*this);
-    ar &tx_id &direction &graph_view;
-  }
-};
-RPC_SINGLE_MEMBER_MESSAGE(CreateBfsSubcursorRes, int64_t);
-
-using CreateBfsSubcursorRpc =
-    communication::rpc::RequestResponse<CreateBfsSubcursorReq,
-                                        CreateBfsSubcursorRes>;
-
-struct RegisterSubcursorsReq : public communication::rpc::Message {
-  std::unordered_map<int, int64_t> subcursor_ids;
-
-  RegisterSubcursorsReq(std::unordered_map<int, int64_t> subcursor_ids)
-      : subcursor_ids(std::move(subcursor_ids)) {}
-
- private:
-  friend class boost::serialization::access;
-
-  RegisterSubcursorsReq() {}
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<communication::rpc::Message>(*this);
-    ar &subcursor_ids;
-  }
-};
-RPC_NO_MEMBER_MESSAGE(RegisterSubcursorsRes);
-
-using RegisterSubcursorsRpc =
-    communication::rpc::RequestResponse<RegisterSubcursorsReq,
-                                        RegisterSubcursorsRes>;
-
-RPC_SINGLE_MEMBER_MESSAGE(RemoveBfsSubcursorReq, int64_t);
-RPC_NO_MEMBER_MESSAGE(RemoveBfsSubcursorRes);
-
-using RemoveBfsSubcursorRpc =
-    communication::rpc::RequestResponse<RemoveBfsSubcursorReq,
-                                        RemoveBfsSubcursorRes>;
-
-RPC_SINGLE_MEMBER_MESSAGE(ExpandLevelReq, int64_t);
-RPC_SINGLE_MEMBER_MESSAGE(ExpandLevelRes, bool);
-
-using ExpandLevelRpc =
-    communication::rpc::RequestResponse<ExpandLevelReq, ExpandLevelRes>;
-
-RPC_SINGLE_MEMBER_MESSAGE(SubcursorPullReq, int64_t);
-
-struct SubcursorPullRes : public communication::rpc::Message {
-  SubcursorPullRes(const VertexAccessor &vertex)
-      : vertex(std::experimental::in_place, vertex) {}
-
-  SubcursorPullRes() : vertex(std::experimental::nullopt) {}
-
-  std::experimental::optional<SerializedVertex> vertex;
-
- private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<communication::rpc::Message>(*this);
-    ar &vertex;
-  }
-};
-
-using SubcursorPullRpc =
-    communication::rpc::RequestResponse<SubcursorPullReq, SubcursorPullRes>;
-
-struct SetSourceReq : public communication::rpc::Message {
-  int64_t subcursor_id;
-  storage::VertexAddress source;
-
-  SetSourceReq(int64_t subcursor_id, storage::VertexAddress source)
-      : subcursor_id(subcursor_id), source(source) {}
-
- private:
-  friend class boost::serialization::access;
-
-  SetSourceReq() {}
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<communication::rpc::Message>(*this);
-    ar &subcursor_id &source;
-  }
-};
-RPC_NO_MEMBER_MESSAGE(SetSourceRes);
-
-using SetSourceRpc =
-    communication::rpc::RequestResponse<SetSourceReq, SetSourceRes>;
-
-struct ExpandToRemoteVertexReq : public communication::rpc::Message {
-  int64_t subcursor_id;
-  storage::EdgeAddress edge;
-  storage::VertexAddress vertex;
-
-  ExpandToRemoteVertexReq(int64_t subcursor_id, storage::EdgeAddress edge,
-                          storage::VertexAddress vertex)
-      : subcursor_id(subcursor_id), edge(edge), vertex(vertex) {}
-
- private:
-  friend class boost::serialization::access;
-
-  ExpandToRemoteVertexReq() {}
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<communication::rpc::Message>(*this);
-    ar &subcursor_id &edge &vertex;
-  }
-};
-RPC_SINGLE_MEMBER_MESSAGE(ExpandToRemoteVertexRes, bool);
-
-using ExpandToRemoteVertexRpc =
-    communication::rpc::RequestResponse<ExpandToRemoteVertexReq,
-                                        ExpandToRemoteVertexRes>;
-
-struct ReconstructPathReq : public communication::rpc::Message {
-  int64_t subcursor_id;
-  std::experimental::optional<storage::VertexAddress> vertex;
-  std::experimental::optional<storage::EdgeAddress> edge;
-
-  ReconstructPathReq(int64_t subcursor_id, storage::VertexAddress vertex)
-      : subcursor_id(subcursor_id),
-        vertex(vertex),
-        edge(std::experimental::nullopt) {}
-
-  ReconstructPathReq(int64_t subcursor_id, storage::EdgeAddress edge)
-      : subcursor_id(subcursor_id),
-        vertex(std::experimental::nullopt),
-        edge(edge) {}
-
- private:
-  friend class boost::serialization::access;
-
-  ReconstructPathReq() {}
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<communication::rpc::Message>(*this);
-    ar &subcursor_id &vertex &edge;
-  }
-};
-
-struct ReconstructPathRes : public communication::rpc::Message {
-  int64_t subcursor_id;
-  std::vector<SerializedEdge> edges;
-  std::experimental::optional<storage::VertexAddress> next_vertex;
-  std::experimental::optional<storage::EdgeAddress> next_edge;
-
-  ReconstructPathRes(
-      const std::vector<EdgeAccessor> &edge_accessors,
-      std::experimental::optional<storage::VertexAddress> next_vertex,
-      std::experimental::optional<storage::EdgeAddress> next_edge)
-      : next_vertex(std::move(next_vertex)), next_edge(std::move(next_edge)) {
-    CHECK(!static_cast<bool>(next_vertex) || !static_cast<bool>(next_edge))
-        << "At most one of `next_vertex` and `next_edge` should be set";
-    for (const auto &edge : edge_accessors) {
-      edges.emplace_back(edge);
-    }
-  }
-
- private:
-  friend class boost::serialization::access;
-
-  ReconstructPathRes() {}
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<communication::rpc::Message>(*this);
-    ar &edges &next_vertex &next_edge;
-  }
-};
-
-using ReconstructPathRpc =
-    communication::rpc::RequestResponse<ReconstructPathReq, ReconstructPathRes>;
-
-struct PrepareForExpandReq : public communication::rpc::Message {
-  int64_t subcursor_id;
-  bool clear;
-
-  PrepareForExpandReq(int64_t subcursor_id, bool clear)
-      : subcursor_id(subcursor_id), clear(clear) {}
-
- private:
-  friend class boost::serialization::access;
-
-  PrepareForExpandReq() {}
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<communication::rpc::Message>(*this);
-    ar &subcursor_id &clear;
-  }
-};
-RPC_NO_MEMBER_MESSAGE(PrepareForExpandRes);
-
-using PrepareForExpandRpc =
-    communication::rpc::RequestResponse<PrepareForExpandReq,
-                                        PrepareForExpandRes>;
-}  // namespace distributed
diff --git a/src/distributed/bfs_rpc_messages.lcp b/src/distributed/bfs_rpc_messages.lcp
new file mode 100644
index 000000000..4cb7c42b7
--- /dev/null
+++ b/src/distributed/bfs_rpc_messages.lcp
@@ -0,0 +1,280 @@
+#>cpp
+#pragma once
+
+#include <tuple>
+
+#include "communication/rpc/messages.hpp"
+#include "distributed/bfs_rpc_messages.capnp.h"
+#include "distributed/bfs_subcursor.hpp"
+#include "query/plan/operator.hpp"
+#include "transactions/type.hpp"
+#include "utils/serialization.hpp"
+cpp<#
+
+(lcp:namespace distributed)
+
+(lcp:capnp-namespace "distributed")
+
+(lcp:capnp-import 'ast "/query/frontend/ast/ast.capnp")
+(lcp:capnp-import 'dis "/distributed/serialization.capnp")
+(lcp:capnp-import 'query "/query/common.capnp")
+(lcp:capnp-import 'storage "/storage/serialization.capnp")
+(lcp:capnp-import 'utils "/utils/serialization.capnp")
+
+(lcp:capnp-type-conversion "storage::EdgeAddress" "Storage.Address")
+(lcp:capnp-type-conversion "storage::VertexAddress" "Storage.Address")
+
+(defun save-element (builder member)
+  #>cpp
+  if (${member}) {
+    if constexpr (std::is_same<TElement, Vertex>::value) {
+      auto builder = ${builder}.initVertex();
+      SaveVertex(*${member}, &builder, worker_id);
+    } else {
+      auto builder = ${builder}.initEdge();
+      SaveEdge(*${member}, &builder, worker_id);
+    }
+  } else {
+    ${builder}.setNull();
+  }
+  cpp<#)
+
+(defun load-element (reader member)
+  (let ((output-member (cl-ppcre:regex-replace "input$" member "output")))
+    #>cpp
+    if (!${reader}.isNull()) {
+      if constexpr (std::is_same<TElement, Vertex>::value) {
+        const auto reader = ${reader}.getVertex();
+        ${output-member} = LoadVertex(reader);
+      } else {
+        const auto reader = ${reader}.getEdge();
+        ${output-member} = LoadEdge(reader);
+      }
+    }
+    cpp<#))
+
+(lcp:define-struct (serialized-graph-element t-element) ()
+  ((global-address "storage::Address<mvcc::VersionList<TElement>>"
+                   :capnp-type "Storage.Address")
+   (old-element-input "TElement *"
+                      :save-fun
+                      "if (old_element_input) {
+                         ar << true;
+                         SaveElement(ar, *old_element_input, worker_id);
+                       } else {
+                         ar << false;
+                       }"
+                      :load-fun ""
+                      :capnp-type '((null "Void") (vertex "Dis.Vertex") (edge "Dis.Edge"))
+                      :capnp-save #'save-element :capnp-load #'load-element)
+   (old-element-output "std::unique_ptr<TElement>"
+                       :save-fun ""
+                       :load-fun
+                       "bool has_old;
+                        ar >> has_old;
+                        if (has_old) {
+                          if constexpr (std::is_same<TElement, Vertex>::value) {
+                            old_element_output = std::move(LoadVertex(ar));
+                          } else {
+                            old_element_output = std::move(LoadEdge(ar));
+                          }
+                        }"
+                       :capnp-save :dont-save)
+   (new-element-input "TElement *"
+                      :save-fun
+                      "if (new_element_input) {
+                         ar << true;
+                         SaveElement(ar, *new_element_input, worker_id);
+                       } else {
+                         ar << false;
+                       }"
+                      :load-fun ""
+                      :capnp-type '((null "Void") (vertex "Dis.Vertex") (edge "Dis.Edge"))
+                      :capnp-save #'save-element :capnp-load #'load-element)
+   (new-element-output "std::unique_ptr<TElement>"
+                       :save-fun ""
+                       :load-fun
+                       "bool has_new;
+                        ar >> has_new;
+                        if (has_new) {
+                          if constexpr (std::is_same<TElement, Vertex>::value) {
+                            new_element_output = std::move(LoadVertex(ar));
+                          } else {
+                            new_element_output = std::move(LoadEdge(ar));
+                          }
+                        }"
+                       :capnp-save :dont-save)
+   (worker-id :int16_t :save-fun "" :load-fun "" :capnp-save :dont-save))
+  (:public
+   #>cpp
+    SerializedGraphElement(storage::Address<mvcc::VersionList<TElement>> global_address,
+                           TElement *old_element_input, TElement *new_element_input,
+                           int16_t worker_id)
+        : global_address(global_address),
+          old_element_input(old_element_input),
+          old_element_output(nullptr),
+          new_element_input(new_element_input),
+          new_element_output(nullptr),
+          worker_id(worker_id) {
+      CHECK(global_address.is_remote())
+          << "Only global addresses should be used with SerializedGraphElement";
+    }
+
+    SerializedGraphElement(const RecordAccessor<TElement> &accessor)
+        : SerializedGraphElement(accessor.GlobalAddress(), accessor.GetOld(),
+                                 accessor.GetNew(),
+                                 accessor.db_accessor().db().WorkerId()) {}
+
+    SerializedGraphElement() {}
+   cpp<#)
+  (:serialize :capnp :type-args '(vertex edge)))
+
+#>cpp
+using SerializedVertex = SerializedGraphElement<Vertex>;
+using SerializedEdge = SerializedGraphElement<Edge>;
+cpp<#
+
+(lcp:define-rpc create-bfs-subcursor
+    (:request
+     ((tx-id "tx::TransactionId" :capnp-type "UInt64")
+      (direction "query::EdgeAtom::Direction"
+                 :capnp-type "Ast.EdgeAtom.Direction" :capnp-init nil
+                 :capnp-save (lcp:capnp-save-enum "::query::capnp::EdgeAtom::Direction"
+                                                  "query::EdgeAtom::Direction"
+                                                  '(in out both))
+                 :capnp-load (lcp:capnp-load-enum "::query::capnp::EdgeAtom::Direction"
+                                                  "query::EdgeAtom::Direction"
+                                                  '(in out both)))
+      ;; TODO(mtomic): Why isn't edge-types serialized?
+      (edge-types "std::vector<storage::EdgeType>"
+                  :save-fun "" :load-fun "" :capnp-save :dont-save)
+      (graph-view "query::GraphView"
+                  :capnp-type "Query.GraphView" :capnp-init nil
+                  :capnp-save (lcp:capnp-save-enum "::query::capnp::GraphView"
+                                                   "query::GraphView"
+                                                   '(old new))
+                  :capnp-load (lcp:capnp-load-enum "::query::capnp::GraphView"
+                                                   "query::GraphView"
+                                                   '(old new)))))
+  (:response ((member :int64_t))))
+
+(lcp:define-rpc register-subcursors
+    (:request ((subcursor-ids "std::unordered_map<int16_t, int64_t>"
+                              :capnp-type "Utils.Map(Utils.BoxInt16, Utils.BoxInt64)"
+                              :capnp-save
+                              (lambda (builder member)
+                                #>cpp
+                                utils::SaveMap<utils::capnp::BoxInt16, utils::capnp::BoxInt64>(
+                                    ${member}, &${builder},
+                                    [](auto *builder, const auto &entry) {
+                                      auto key_builder = builder->initKey();
+                                      key_builder.setValue(entry.first);
+                                      auto value_builder = builder->initValue();
+                                      value_builder.setValue(entry.second);
+                                    });
+                                cpp<#)
+                              :capnp-load
+                              (lambda (reader member)
+                                #>cpp
+                                utils::LoadMap<utils::capnp::BoxInt16, utils::capnp::BoxInt64>(
+                                    &${member}, ${reader},
+                                    [](const auto &reader) {
+                                      int16_t key = reader.getKey().getValue();
+                                      int64_t value = reader.getValue().getValue();
+                                      return std::make_pair(key, value);
+                                    });
+                                cpp<#))))
+  (:response ()))
+
+(lcp:define-rpc remove-bfs-subcursor
+    (:request ((member :int64_t)))
+  (:response ()))
+
+(lcp:define-rpc expand-level
+    (:request ((member :int64_t)))
+  (:response ((member :bool))))
+
+(lcp:define-rpc subcursor-pull
+    (:request ((member :int64_t)))
+  (:response ((vertex "std::experimental::optional<SerializedVertex>" :initarg :move
+                      :capnp-type "Utils.Optional(SerializedGraphElement)"
+                      :capnp-save (lcp:capnp-save-optional "capnp::SerializedGraphElement" "SerializedVertex")
+                      :capnp-load (lcp:capnp-load-optional "capnp::SerializedGraphElement" "SerializedVertex")))))
+(lcp:define-rpc set-source
+    (:request
+     ((subcursor-id :int64_t)
+      (source "storage::VertexAddress")))
+  (:response ()))
+
+(lcp:define-rpc expand-to-remote-vertex
+    (:request
+     ((subcursor-id :int64_t)
+      (edge "storage::EdgeAddress")
+      (vertex "storage::VertexAddress")))
+  (:response ((member :bool))))
+
+(lcp:define-rpc reconstruct-path
+    (:request
+     ((subcursor-id :int64_t)
+      (vertex "std::experimental::optional<storage::VertexAddress>"
+              :capnp-save (lcp:capnp-save-optional "storage::capnp::Address" "storage::VertexAddress")
+              :capnp-load (lcp:capnp-load-optional "storage::capnp::Address" "storage::VertexAddress"))
+      (edge "std::experimental::optional<storage::EdgeAddress>"
+            :capnp-save (lcp:capnp-save-optional "storage::capnp::Address" "storage::EdgeAddress")
+            :capnp-load (lcp:capnp-load-optional "storage::capnp::Address" "storage::EdgeAddress")))
+     (:public
+      #>cpp
+      using Capnp = capnp::ReconstructPathReq;
+      static const communication::rpc::MessageType TypeInfo;
+
+      ReconstructPathReq() {}
+
+      ReconstructPathReq(int64_t subcursor_id, storage::VertexAddress vertex)
+          : subcursor_id(subcursor_id),
+            vertex(vertex),
+            edge(std::experimental::nullopt) {}
+
+      ReconstructPathReq(int64_t subcursor_id, storage::EdgeAddress edge)
+          : subcursor_id(subcursor_id),
+            vertex(std::experimental::nullopt),
+            edge(edge) {}
+      cpp<#))
+  (:response
+   ((subcursor-id :int64_t ;; TODO(mtomic): Unused?
+                  :save-fun "" :load-fun "" :capnp-save :dont-save)
+    (edges "std::vector<SerializedEdge>" :capnp-type "List(SerializedGraphElement)"
+           :capnp-save (lcp:capnp-save-vector "capnp::SerializedGraphElement" "SerializedEdge")
+           :capnp-load (lcp:capnp-load-vector "capnp::SerializedGraphElement" "SerializedEdge"))
+    (next-vertex "std::experimental::optional<storage::VertexAddress>"
+                 :capnp-save (lcp:capnp-save-optional "storage::capnp::Address" "storage::VertexAddress")
+                 :capnp-load (lcp:capnp-load-optional "storage::capnp::Address" "storage::VertexAddress"))
+    (next-edge "std::experimental::optional<storage::EdgeAddress>"
+               :capnp-save (lcp:capnp-save-optional "storage::capnp::Address" "storage::EdgeAddress")
+               :capnp-load (lcp:capnp-load-optional "storage::capnp::Address" "storage::EdgeAddress")))
+   (:public
+    #>cpp
+    using Capnp = capnp::ReconstructPathRes;
+    static const communication::rpc::MessageType TypeInfo;
+
+    ReconstructPathRes() {}
+
+    ReconstructPathRes(
+        const std::vector<EdgeAccessor> &edge_accessors,
+        std::experimental::optional<storage::VertexAddress> next_vertex,
+        std::experimental::optional<storage::EdgeAddress> next_edge)
+        : next_vertex(std::move(next_vertex)), next_edge(std::move(next_edge)) {
+      CHECK(!static_cast<bool>(next_vertex) || !static_cast<bool>(next_edge))
+          << "At most one of `next_vertex` and `next_edge` should be set";
+      for (const auto &edge : edge_accessors) {
+        edges.emplace_back(edge);
+      }
+    }
+    cpp<#)))
+
+(lcp:define-rpc prepare-for-expand
+    (:request
+     ((subcursor-id :int64_t)
+      (clear :bool)))
+  (:response ()))
+
+(lcp:pop-namespace) ;; distributed
diff --git a/src/distributed/bfs_rpc_server.hpp b/src/distributed/bfs_rpc_server.hpp
index ff9b38230..2c6832030 100644
--- a/src/distributed/bfs_rpc_server.hpp
+++ b/src/distributed/bfs_rpc_server.hpp
@@ -20,52 +20,78 @@ class BfsRpcServer {
                BfsSubcursorStorage *subcursor_storage)
       : db_(db), server_(server), subcursor_storage_(subcursor_storage) {
     server_->Register<CreateBfsSubcursorRpc>(
-        [this](const CreateBfsSubcursorReq &req) {
-          return std::make_unique<CreateBfsSubcursorRes>(
-              subcursor_storage_->Create(req.tx_id, req.direction,
-                                         req.edge_types, req.graph_view));
+        [this](const auto &req_reader, auto *res_builder) {
+          CreateBfsSubcursorReq req;
+          req.Load(req_reader);
+          CreateBfsSubcursorRes res(subcursor_storage_->Create(
+              req.tx_id, req.direction, req.edge_types, req.graph_view));
+          res.Save(res_builder);
         });
 
     server_->Register<RegisterSubcursorsRpc>(
-        [this](const RegisterSubcursorsReq &req) {
+        [this](const auto &req_reader, auto *res_builder) {
+          RegisterSubcursorsReq req;
+          req.Load(req_reader);
           subcursor_storage_->Get(req.subcursor_ids.at(db_->WorkerId()))
               ->RegisterSubcursors(req.subcursor_ids);
-          return std::make_unique<RegisterSubcursorsRes>();
+          RegisterSubcursorsRes res;
+          res.Save(res_builder);
         });
 
     server_->Register<RemoveBfsSubcursorRpc>(
-        [this](const RemoveBfsSubcursorReq &req) {
+        [this](const auto &req_reader, auto *res_builder) {
+          RemoveBfsSubcursorReq req;
+          req.Load(req_reader);
           subcursor_storage_->Erase(req.member);
-          return std::make_unique<RemoveBfsSubcursorRes>();
+          RemoveBfsSubcursorRes res;
+          res.Save(res_builder);
         });
 
-    server_->Register<SetSourceRpc>([this](const SetSourceReq &req) {
-      subcursor_storage_->Get(req.subcursor_id)->SetSource(req.source);
-      return std::make_unique<SetSourceRes>();
+    server_->Register<SetSourceRpc>(
+        [this](const auto &req_reader, auto *res_builder) {
+          SetSourceReq req;
+          req.Load(req_reader);
+          subcursor_storage_->Get(req.subcursor_id)->SetSource(req.source);
+          SetSourceRes res;
+          res.Save(res_builder);
+        });
+
+    server_->Register<ExpandLevelRpc>([this](const auto &req_reader,
+                                             auto *res_builder) {
+      ExpandLevelReq req;
+      req.Load(req_reader);
+      ExpandLevelRes res(subcursor_storage_->Get(req.member)->ExpandLevel());
+      res.Save(res_builder);
     });
 
-    server_->Register<ExpandLevelRpc>([this](const ExpandLevelReq &req) {
-      return std::make_unique<ExpandLevelRes>(
-          subcursor_storage_->Get(req.member)->ExpandLevel());
-    });
-
-    server_->Register<SubcursorPullRpc>([this](const SubcursorPullReq &req) {
-      auto vertex = subcursor_storage_->Get(req.member)->Pull();
-      if (!vertex) {
-        return std::make_unique<SubcursorPullRes>();
-      }
-      return std::make_unique<SubcursorPullRes>(*vertex);
-    });
+    server_->Register<SubcursorPullRpc>(
+        [this](const auto &req_reader, auto *res_builder) {
+          SubcursorPullReq req;
+          req.Load(req_reader);
+          auto vertex = subcursor_storage_->Get(req.member)->Pull();
+          if (!vertex) {
+            SubcursorPullRes res;
+            res.Save(res_builder);
+            return;
+          }
+          SubcursorPullRes res(*vertex);
+          res.Save(res_builder);
+        });
 
     server_->Register<ExpandToRemoteVertexRpc>(
-        [this](const ExpandToRemoteVertexReq &req) {
-          return std::make_unique<ExpandToRemoteVertexRes>(
+        [this](const auto &req_reader, auto *res_builder) {
+          ExpandToRemoteVertexReq req;
+          req.Load(req_reader);
+          ExpandToRemoteVertexRes res(
               subcursor_storage_->Get(req.subcursor_id)
                   ->ExpandToLocalVertex(req.edge, req.vertex));
+          res.Save(res_builder);
         });
 
-    server_->Register<ReconstructPathRpc>([this](
-                                              const ReconstructPathReq &req) {
+    server_->Register<ReconstructPathRpc>([this](const auto &req_reader,
+                                                 auto *res_builder) {
+      ReconstructPathReq req;
+      req.Load(req_reader);
       auto subcursor = subcursor_storage_->Get(req.subcursor_id);
       PathSegment result;
       if (req.vertex) {
@@ -75,14 +101,18 @@ class BfsRpcServer {
       } else {
         LOG(FATAL) << "`edge` or `vertex` should be set in ReconstructPathReq";
       }
-      return std::make_unique<ReconstructPathRes>(
-          result.edges, result.next_vertex, result.next_edge);
+      ReconstructPathRes res(result.edges, result.next_vertex,
+                             result.next_edge);
+      res.Save(res_builder);
     });
 
-    server_->Register<PrepareForExpandRpc>([this](
-                                               const PrepareForExpandReq &req) {
+    server_->Register<PrepareForExpandRpc>([this](const auto &req_reader,
+                                                  auto *res_builder) {
+      PrepareForExpandReq req;
+      req.Load(req_reader);
       subcursor_storage_->Get(req.subcursor_id)->PrepareForExpand(req.clear);
-      return std::make_unique<PrepareForExpandRes>();
+      PrepareForExpandRes res;
+      res.Save(res_builder);
     });
   }
 
diff --git a/src/distributed/bfs_subcursor.hpp b/src/distributed/bfs_subcursor.hpp
index d92e40692..7959e537d 100644
--- a/src/distributed/bfs_subcursor.hpp
+++ b/src/distributed/bfs_subcursor.hpp
@@ -35,7 +35,7 @@ class ExpandBfsSubcursor {
                      query::GraphView graph_view);
 
   // Stores subcursor ids of other workers.
-  void RegisterSubcursors(std::unordered_map<int, int64_t> subcursor_ids) {
+  void RegisterSubcursors(std::unordered_map<int16_t, int64_t> subcursor_ids) {
     subcursor_ids_ = std::move(subcursor_ids);
   }
 
@@ -91,7 +91,7 @@ class ExpandBfsSubcursor {
   database::GraphDbAccessor dba_;
 
   /// IDs of subcursors on other workers, used when sending RPCs.
-  std::unordered_map<int, int64_t> subcursor_ids_;
+  std::unordered_map<int16_t, int64_t> subcursor_ids_;
 
   query::EdgeAtom::Direction direction_;
   std::vector<storage::EdgeType> edge_types_;
diff --git a/src/distributed/cluster_discovery_master.cpp b/src/distributed/cluster_discovery_master.cpp
index 53e48d208..9c03a1e6f 100644
--- a/src/distributed/cluster_discovery_master.cpp
+++ b/src/distributed/cluster_discovery_master.cpp
@@ -11,7 +11,10 @@ ClusterDiscoveryMaster::ClusterDiscoveryMaster(
     : server_(server),
       coordination_(coordination),
       rpc_worker_clients_(rpc_worker_clients) {
-  server_.Register<RegisterWorkerRpc>([this](const RegisterWorkerReq &req) {
+  server_.Register<RegisterWorkerRpc>([this](const auto &req_reader,
+                                             auto *res_builder) {
+    RegisterWorkerReq req;
+    req.Load(req_reader);
     bool registration_successful =
         this->coordination_.RegisterWorker(req.desired_worker_id, req.endpoint);
 
@@ -24,15 +27,15 @@ ClusterDiscoveryMaster::ClusterDiscoveryMaster(
           });
     }
 
-    return std::make_unique<RegisterWorkerRes>(
-        registration_successful, this->coordination_.RecoveryInfo(),
-        this->coordination_.GetWorkers());
+    RegisterWorkerRes res(registration_successful,
+                          this->coordination_.RecoveryInfo(),
+                          this->coordination_.GetWorkers());
+    res.Save(res_builder);
   });
 
   server_.Register<NotifyWorkerRecoveredRpc>(
-      [this](const NotifyWorkerRecoveredReq &req) {
-        this->coordination_.WorkerRecovered(req.member);
-        return std::make_unique<NotifyWorkerRecoveredRes>();
+      [this](const auto &req_reader, auto *res_builder) {
+        this->coordination_.WorkerRecovered(req_reader.getMember());
       });
 }
 
diff --git a/src/distributed/cluster_discovery_worker.cpp b/src/distributed/cluster_discovery_worker.cpp
index 7184c3ab4..85746797c 100644
--- a/src/distributed/cluster_discovery_worker.cpp
+++ b/src/distributed/cluster_discovery_worker.cpp
@@ -8,10 +8,12 @@ ClusterDiscoveryWorker::ClusterDiscoveryWorker(
     Server &server, WorkerCoordination &coordination,
     communication::rpc::ClientPool &client_pool)
     : server_(server), coordination_(coordination), client_pool_(client_pool) {
-  server_.Register<ClusterDiscoveryRpc>([this](const ClusterDiscoveryReq &req) {
-    this->coordination_.RegisterWorker(req.worker_id, req.endpoint);
-    return std::make_unique<ClusterDiscoveryRes>();
-  });
+  server_.Register<ClusterDiscoveryRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        ClusterDiscoveryReq req;
+        req.Load(req_reader);
+        this->coordination_.RegisterWorker(req.worker_id, req.endpoint);
+      });
 }
 
 void ClusterDiscoveryWorker::RegisterWorker(int worker_id) {
diff --git a/src/distributed/coordination_rpc_messages.hpp b/src/distributed/coordination_rpc_messages.hpp
deleted file mode 100644
index 4bd94b5fc..000000000
--- a/src/distributed/coordination_rpc_messages.hpp
+++ /dev/null
@@ -1,101 +0,0 @@
-#pragma once
-
-#include <experimental/optional>
-#include <unordered_map>
-
-#include "boost/serialization/access.hpp"
-#include "boost/serialization/base_object.hpp"
-#include "boost/serialization/unordered_map.hpp"
-
-#include "communication/rpc/messages.hpp"
-#include "durability/recovery.hpp"
-#include "io/network/endpoint.hpp"
-
-namespace distributed {
-
-using communication::rpc::Message;
-using Endpoint = io::network::Endpoint;
-
-struct RegisterWorkerReq : public Message {
-  // Set desired_worker_id to -1 to get an automatically assigned ID.
-  RegisterWorkerReq(int desired_worker_id, const Endpoint &endpoint)
-      : desired_worker_id(desired_worker_id), endpoint(endpoint) {}
-  int desired_worker_id;
-  Endpoint endpoint;
-
- private:
-  friend class boost::serialization::access;
-  RegisterWorkerReq() {}
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<Message>(*this);
-    ar &desired_worker_id;
-    ar &endpoint;
-  }
-};
-
-struct RegisterWorkerRes : public Message {
-  RegisterWorkerRes(
-      bool registration_successful,
-      std::experimental::optional<durability::RecoveryInfo> recovery_info,
-      std::unordered_map<int, Endpoint> workers)
-      : registration_successful(registration_successful),
-        recovery_info(recovery_info),
-        workers(std::move(workers)) {}
-
-  bool registration_successful;
-  std::experimental::optional<durability::RecoveryInfo> recovery_info;
-  std::unordered_map<int, Endpoint> workers;
-
- private:
-  friend class boost::serialization::access;
-  RegisterWorkerRes() {}
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<Message>(*this);
-    ar &registration_successful;
-    ar &recovery_info;
-    ar &workers;
-  }
-};
-
-struct ClusterDiscoveryReq : public Message {
-  ClusterDiscoveryReq(int worker_id, Endpoint endpoint)
-      : worker_id(worker_id), endpoint(endpoint) {}
-
-  int worker_id;
-  Endpoint endpoint;
-
- private:
-  friend class boost::serialization::access;
-  ClusterDiscoveryReq() {}
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<Message>(*this);
-    ar &worker_id;
-    ar &endpoint;
-  }
-};
-
-RPC_NO_MEMBER_MESSAGE(ClusterDiscoveryRes);
-
-RPC_NO_MEMBER_MESSAGE(StopWorkerReq);
-RPC_NO_MEMBER_MESSAGE(StopWorkerRes);
-
-RPC_SINGLE_MEMBER_MESSAGE(NotifyWorkerRecoveredReq, int);
-RPC_NO_MEMBER_MESSAGE(NotifyWorkerRecoveredRes);
-
-using RegisterWorkerRpc =
-    communication::rpc::RequestResponse<RegisterWorkerReq, RegisterWorkerRes>;
-using StopWorkerRpc =
-    communication::rpc::RequestResponse<StopWorkerReq, StopWorkerRes>;
-using NotifyWorkerRecoveredRpc =
-    communication::rpc::RequestResponse<NotifyWorkerRecoveredReq,
-                                        NotifyWorkerRecoveredRes>;
-using ClusterDiscoveryRpc =
-    communication::rpc::RequestResponse<ClusterDiscoveryReq,
-                                        ClusterDiscoveryRes>;
-}  // namespace distributed
diff --git a/src/distributed/coordination_rpc_messages.lcp b/src/distributed/coordination_rpc_messages.lcp
new file mode 100644
index 000000000..8237740cb
--- /dev/null
+++ b/src/distributed/coordination_rpc_messages.lcp
@@ -0,0 +1,72 @@
+#>cpp
+#pragma once
+
+#include <experimental/optional>
+#include <unordered_map>
+
+#include "communication/rpc/messages.hpp"
+#include "distributed/coordination_rpc_messages.capnp.h"
+#include "durability/recovery.hpp"
+#include "io/network/endpoint.hpp"
+cpp<#
+
+(lcp:namespace distributed)
+
+(lcp:capnp-namespace "distributed")
+
+(lcp:capnp-import 'dur "/durability/recovery.capnp")
+(lcp:capnp-import 'io "/io/network/endpoint.capnp")
+(lcp:capnp-import 'utils "/utils/serialization.capnp")
+
+(lcp:define-rpc register-worker
+    (:request
+     ((desired-worker-id :int16_t)
+      (endpoint "io::network::Endpoint" :capnp-type "Io.Endpoint")))
+  (:response
+   ((registration-successful :bool)
+    (recovery-info "std::experimental::optional<durability::RecoveryInfo>"
+                   :capnp-type "Utils.Optional(Dur.RecoveryInfo)"
+                   :capnp-save (lcp:capnp-save-optional "durability::capnp::RecoveryInfo"
+                                                        "durability::RecoveryInfo")
+                   :capnp-load (lcp:capnp-load-optional "durability::capnp::RecoveryInfo"
+                                                        "durability::RecoveryInfo"))
+    (workers "std::unordered_map<int, io::network::Endpoint>"
+             :capnp-type "Utils.Map(Utils.BoxInt16, Io.Endpoint)"
+             :capnp-save
+             (lambda (builder member)
+               #>cpp
+               utils::SaveMap<utils::capnp::BoxInt16, io::network::capnp::Endpoint>(${member}, &${builder},
+                              [](auto *builder, const auto &entry) {
+                                auto key_builder = builder->initKey();
+                                key_builder.setValue(entry.first);
+                                auto value_builder = builder->initValue();
+                                entry.second.Save(&value_builder);
+                              });
+               cpp<#)
+             :capnp-load
+             (lambda (reader member)
+               #>cpp
+               utils::LoadMap<utils::capnp::BoxInt16, io::network::capnp::Endpoint>(&${member}, ${reader},
+                              [](const auto &reader) {
+                                io::network::Endpoint value;
+                                value.Load(reader.getValue());
+                                return std::make_pair(reader.getKey().getValue(), value);
+                              });
+               cpp<#)))))
+
+(lcp:define-rpc cluster-discovery
+    (:request
+     ((worker-id :int16_t)
+      (endpoint "io::network::Endpoint" :capnp-type "Io.Endpoint")))
+  (:response ()))
+
+(lcp:define-rpc stop-worker
+    (:request ())
+  (:response ()))
+
+(lcp:define-rpc notify-worker-recovered
+    (:request ((member :int64_t)))
+  (:response ()))
+
+(lcp:pop-namespace) ;; distributed
+
diff --git a/src/distributed/coordination_worker.cpp b/src/distributed/coordination_worker.cpp
index 4ae35a923..a094a20c4 100644
--- a/src/distributed/coordination_worker.cpp
+++ b/src/distributed/coordination_worker.cpp
@@ -27,19 +27,18 @@ void WorkerCoordination::WaitForShutdown() {
   std::condition_variable cv;
   bool shutdown = false;
 
-  server_.Register<StopWorkerRpc>([&](const StopWorkerReq &) {
+  server_.Register<StopWorkerRpc>([&](const auto &req_reader, auto *res_builder) {
     std::unique_lock<std::mutex> lk(mutex);
     shutdown = true;
     lk.unlock();
     cv.notify_one();
-    return std::make_unique<StopWorkerRes>();
   });
 
   std::unique_lock<std::mutex> lk(mutex);
   cv.wait(lk, [&shutdown] { return shutdown; });
 }
 
-Endpoint WorkerCoordination::GetEndpoint(int worker_id) {
+io::network::Endpoint WorkerCoordination::GetEndpoint(int worker_id) {
   std::lock_guard<std::mutex> guard(lock_);
   return Coordination::GetEndpoint(worker_id);
 }
diff --git a/src/distributed/data_rpc_clients.cpp b/src/distributed/data_rpc_clients.cpp
index 5ee6fae8a..ac3ffa4ff 100644
--- a/src/distributed/data_rpc_clients.cpp
+++ b/src/distributed/data_rpc_clients.cpp
@@ -14,7 +14,7 @@ std::unique_ptr<Edge> DataRpcClients::RemoteElement(int worker_id,
   auto response =
       clients_.GetClientPool(worker_id).Call<EdgeRpc>(TxGidPair{tx_id, gid});
   CHECK(response) << "EdgeRpc failed";
-  return std::move(response->name_output_);
+  return std::move(response->edge_output);
 }
 
 template <>
@@ -24,7 +24,7 @@ std::unique_ptr<Vertex> DataRpcClients::RemoteElement(int worker_id,
   auto response =
       clients_.GetClientPool(worker_id).Call<VertexRpc>(TxGidPair{tx_id, gid});
   CHECK(response) << "VertexRpc failed";
-  return std::move(response->name_output_);
+  return std::move(response->vertex_output);
 }
 
 std::unordered_map<int, int64_t> DataRpcClients::VertexCounts(
diff --git a/src/distributed/data_rpc_messages.hpp b/src/distributed/data_rpc_messages.hpp
deleted file mode 100644
index 60d7a79e8..000000000
--- a/src/distributed/data_rpc_messages.hpp
+++ /dev/null
@@ -1,72 +0,0 @@
-#pragma once
-
-#include <memory>
-#include <string>
-
-#include "communication/rpc/messages.hpp"
-#include "distributed/serialization.hpp"
-#include "storage/edge.hpp"
-#include "storage/gid.hpp"
-#include "storage/vertex.hpp"
-#include "transactions/type.hpp"
-
-namespace distributed {
-
-struct TxGidPair {
-  tx::TransactionId tx_id;
-  gid::Gid gid;
-
- private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &tx_id;
-    ar &gid;
-  }
-};
-
-#define MAKE_RESPONSE(type, name)                                           \
-  class type##Res : public communication::rpc::Message {                    \
-   public:                                                                  \
-    type##Res() {}                                                          \
-    type##Res(const type *name, int worker_id)                              \
-        : name_input_(name), worker_id_(worker_id) {}                       \
-                                                                            \
-    template <class TArchive>                                               \
-    void save(TArchive &ar, unsigned int) const {                           \
-      ar << boost::serialization::base_object<                              \
-          const communication::rpc::Message>(*this);                        \
-      Save##type(ar, *name_input_, worker_id_);                             \
-    }                                                                       \
-                                                                            \
-    template <class TArchive>                                               \
-    void load(TArchive &ar, unsigned int) {                                 \
-      ar >> boost::serialization::base_object<communication::rpc::Message>( \
-                *this);                                                     \
-      auto v = Load##type(ar);                                              \
-      v.swap(name_output_);                                                 \
-    }                                                                       \
-    BOOST_SERIALIZATION_SPLIT_MEMBER()                                      \
-                                                                            \
-    const type *name_input_;                                                \
-    int worker_id_;                                                         \
-    std::unique_ptr<type> name_output_;                                     \
-  };
-
-MAKE_RESPONSE(Vertex, vertex)
-MAKE_RESPONSE(Edge, edge)
-
-#undef MAKE_RESPONSE
-
-RPC_SINGLE_MEMBER_MESSAGE(VertexReq, TxGidPair);
-RPC_SINGLE_MEMBER_MESSAGE(EdgeReq, TxGidPair);
-RPC_SINGLE_MEMBER_MESSAGE(VertexCountReq, tx::TransactionId);
-RPC_SINGLE_MEMBER_MESSAGE(VertexCountRes, int64_t);
-
-using VertexRpc = communication::rpc::RequestResponse<VertexReq, VertexRes>;
-using EdgeRpc = communication::rpc::RequestResponse<EdgeReq, EdgeRes>;
-using VertexCountRpc =
-    communication::rpc::RequestResponse<VertexCountReq, VertexCountRes>;
-
-}  // namespace distributed
diff --git a/src/distributed/data_rpc_messages.lcp b/src/distributed/data_rpc_messages.lcp
new file mode 100644
index 000000000..5f0f1ca3f
--- /dev/null
+++ b/src/distributed/data_rpc_messages.lcp
@@ -0,0 +1,76 @@
+#>cpp
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include "communication/rpc/messages.hpp"
+#include "distributed/data_rpc_messages.capnp.h"
+#include "distributed/serialization.hpp"
+#include "storage/edge.hpp"
+#include "storage/gid.hpp"
+#include "storage/vertex.hpp"
+#include "transactions/type.hpp"
+cpp<#
+
+(lcp:namespace distributed)
+
+(lcp:capnp-namespace "distributed")
+
+(lcp:capnp-import 'utils "/utils/serialization.capnp")
+(lcp:capnp-import 'dist "/distributed/serialization.capnp")
+
+(lcp:define-struct tx-gid-pair ()
+  ((tx-id "tx::TransactionId" :capnp-type "UInt64")
+   (gid "gid::Gid" :capnp-type "UInt64"))
+  (:serialize :capnp))
+
+(lcp:define-rpc vertex
+    (:request ((member "TxGidPair")))
+  (:response
+   ((vertex-input "const Vertex *"
+                  :save-fun "SaveVertex(ar, *vertex_input, worker_id);" :load-fun ""
+                  :capnp-type "Dist.Vertex"
+                  :capnp-save
+                  (lambda (builder member)
+                    #>cpp
+                    SaveVertex(*${member}, &${builder}, worker_id);
+                    cpp<#)
+                  :capnp-load
+                  (lambda (reader member)
+                    (declare (ignore member))
+                    #>cpp
+                    vertex_output = LoadVertex<const capnp::Vertex::Reader>(${reader});
+                    cpp<#))
+    (worker-id :int64_t :save-fun "" :load-fun "" :capnp-save :dont-save)
+    (vertex-output "std::unique_ptr<Vertex>" :initarg nil
+                   :save-fun "" :load-fun "vertex_output = LoadVertex(ar);"
+                   :capnp-save :dont-save))))
+
+(lcp:define-rpc edge
+    (:request ((member "TxGidPair")))
+  (:response
+   ((edge-input "const Edge *"
+                :save-fun "SaveEdge(ar, *edge_input, worker_id);" :load-fun ""
+                :capnp-type "Dist.Edge"
+                :capnp-save
+                (lambda (builder member)
+                  #>cpp
+                  SaveEdge(*${member}, &${builder}, worker_id);
+                  cpp<#)
+                :capnp-load
+                (lambda (reader member)
+                  (declare (ignore member))
+                  #>cpp
+                  edge_output = LoadEdge<const capnp::Edge::Reader>(${reader});
+                  cpp<#))
+    (worker-id :int64_t :save-fun "" :load-fun "" :capnp-save :dont-save)
+    (edge-output "std::unique_ptr<Edge>" :initarg nil
+                 :save-fun "" :load-fun "edge_output = LoadEdge(ar);"
+                 :capnp-save :dont-save))))
+
+(lcp:define-rpc vertex-count
+    (:request ((member "tx::TransactionId" :capnp-type "UInt64")))
+  (:response ((member :int64_t))))
+
+(lcp:pop-namespace) ;; distributed
diff --git a/src/distributed/data_rpc_server.cpp b/src/distributed/data_rpc_server.cpp
index 70e67e8f1..62c09ce4c 100644
--- a/src/distributed/data_rpc_server.cpp
+++ b/src/distributed/data_rpc_server.cpp
@@ -10,27 +10,34 @@ DataRpcServer::DataRpcServer(database::GraphDb &db,
                              communication::rpc::Server &server)
     : db_(db), rpc_server_(server) {
   rpc_server_.Register<VertexRpc>(
-      [this](const VertexReq &req) {
-        database::GraphDbAccessor dba(db_, req.member.tx_id);
-        auto vertex = dba.FindVertex(req.member.gid, false);
+      [this](const auto &req_reader, auto *res_builder) {
+        database::GraphDbAccessor dba(db_, req_reader.getMember().getTxId());
+        auto vertex = dba.FindVertex(req_reader.getMember().getGid(), false);
         CHECK(vertex.GetOld())
             << "Old record must exist when sending vertex by RPC";
-        return std::make_unique<VertexRes>(vertex.GetOld(), db_.WorkerId());
+        VertexRes response(vertex.GetOld(), db_.WorkerId());
+        response.Save(res_builder);
       });
 
-  rpc_server_.Register<EdgeRpc>([this](const EdgeReq &req) {
-    database::GraphDbAccessor dba(db_, req.member.tx_id);
-    auto edge = dba.FindEdge(req.member.gid, false);
+  rpc_server_.Register<EdgeRpc>([this](const auto &req_reader,
+                                       auto *res_builder) {
+    database::GraphDbAccessor dba(db_, req_reader.getMember().getTxId());
+    auto edge = dba.FindEdge(req_reader.getMember().getGid(), false);
     CHECK(edge.GetOld()) << "Old record must exist when sending edge by RPC";
-    return std::make_unique<EdgeRes>(edge.GetOld(), db_.WorkerId());
+    EdgeRes response(edge.GetOld(), db_.WorkerId());
+    response.Save(res_builder);
   });
 
-  rpc_server_.Register<VertexCountRpc>([this](const VertexCountReq &req) {
-    database::GraphDbAccessor dba(db_, req.member);
-    int64_t size = 0;
-    for (auto vertex : dba.Vertices(false)) ++size;
-    return std::make_unique<VertexCountRes>(size);
-  });
+  rpc_server_.Register<VertexCountRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        VertexCountReq req;
+        req.Load(req_reader);
+        database::GraphDbAccessor dba(db_, req.member);
+        int64_t size = 0;
+        for (auto vertex : dba.Vertices(false)) ++size;
+        VertexCountRes res(size);
+        res.Save(res_builder);
+      });
 }
 
 }  // namespace distributed
diff --git a/src/distributed/durability_rpc_clients.cpp b/src/distributed/durability_rpc_clients.cpp
index 866f63efb..660965cba 100644
--- a/src/distributed/durability_rpc_clients.cpp
+++ b/src/distributed/durability_rpc_clients.cpp
@@ -10,7 +10,7 @@ utils::Future<bool> DurabilityRpcClients::MakeSnapshot(tx::TransactionId tx) {
     auto futures = clients_.ExecuteOnWorkers<bool>(
         0, [tx](int worker_id, communication::rpc::ClientPool &client_pool) {
           auto res = client_pool.Call<MakeSnapshotRpc>(tx);
-          if (res == nullptr) return false;
+          if (!res) return false;
           return res->member;
         });
 
diff --git a/src/distributed/durability_rpc_messages.hpp b/src/distributed/durability_rpc_messages.hpp
deleted file mode 100644
index baf147814..000000000
--- a/src/distributed/durability_rpc_messages.hpp
+++ /dev/null
@@ -1,17 +0,0 @@
-#pragma once
-
-#include "boost/serialization/access.hpp"
-#include "boost/serialization/base_object.hpp"
-
-#include "communication/rpc/messages.hpp"
-#include "transactions/transaction.hpp"
-
-namespace distributed {
-
-RPC_SINGLE_MEMBER_MESSAGE(MakeSnapshotReq, tx::TransactionId);
-RPC_SINGLE_MEMBER_MESSAGE(MakeSnapshotRes, bool);
-
-using MakeSnapshotRpc =
-    communication::rpc::RequestResponse<MakeSnapshotReq, MakeSnapshotRes>;
-
-}  // namespace distributed
diff --git a/src/distributed/durability_rpc_messages.lcp b/src/distributed/durability_rpc_messages.lcp
new file mode 100644
index 000000000..9027569f1
--- /dev/null
+++ b/src/distributed/durability_rpc_messages.lcp
@@ -0,0 +1,20 @@
+#>cpp
+#pragma once
+
+#include "boost/serialization/access.hpp"
+#include "boost/serialization/base_object.hpp"
+
+#include "communication/rpc/messages.hpp"
+#include "distributed/durability_rpc_messages.capnp.h"
+#include "transactions/transaction.hpp"
+cpp<#
+
+(lcp:namespace distributed)
+
+(lcp:capnp-namespace "distributed")
+
+(lcp:define-rpc make-snapshot
+    (:request ((member "tx::TransactionId" :capnp-type "UInt64")))
+  (:response ((member :bool))))
+
+(lcp:pop-namespace) ;; distributed
diff --git a/src/distributed/durability_rpc_server.cpp b/src/distributed/durability_rpc_server.cpp
index 801b59b16..031dc73dc 100644
--- a/src/distributed/durability_rpc_server.cpp
+++ b/src/distributed/durability_rpc_server.cpp
@@ -9,10 +9,12 @@ namespace distributed {
 DurabilityRpcServer::DurabilityRpcServer(database::GraphDb &db,
                                          communication::rpc::Server &server)
     : db_(db), rpc_server_(server) {
-  rpc_server_.Register<MakeSnapshotRpc>([this](const MakeSnapshotReq &req) {
-    database::GraphDbAccessor dba(this->db_, req.member);
-    return std::make_unique<MakeSnapshotRes>(this->db_.MakeSnapshot(dba));
-  });
+  rpc_server_.Register<MakeSnapshotRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        database::GraphDbAccessor dba(this->db_, req_reader.getMember());
+        MakeSnapshotRes res(this->db_.MakeSnapshot(dba));
+        res.Save(res_builder);
+      });
 }
 
 }  // namespace distributed
diff --git a/src/distributed/index_rpc_messages.hpp b/src/distributed/index_rpc_messages.hpp
deleted file mode 100644
index 3f9ebf321..000000000
--- a/src/distributed/index_rpc_messages.hpp
+++ /dev/null
@@ -1,32 +0,0 @@
-#pragma once
-
-#include <memory>
-#include <string>
-
-#include "communication/rpc/messages.hpp"
-#include "distributed/serialization.hpp"
-
-namespace distributed {
-
-struct IndexLabelPropertyTx {
-  storage::Label label;
-  storage::Property property;
-  tx::TransactionId tx_id;
-
- private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &label;
-    ar &property;
-    ar &tx_id;
-  }
-};
-
-RPC_SINGLE_MEMBER_MESSAGE(BuildIndexReq, IndexLabelPropertyTx);
-RPC_NO_MEMBER_MESSAGE(BuildIndexRes);
-
-using BuildIndexRpc =
-    communication::rpc::RequestResponse<BuildIndexReq, BuildIndexRes>;
-}  // namespace distributed
diff --git a/src/distributed/index_rpc_messages.lcp b/src/distributed/index_rpc_messages.lcp
new file mode 100644
index 000000000..d1573b53a
--- /dev/null
+++ b/src/distributed/index_rpc_messages.lcp
@@ -0,0 +1,25 @@
+#>cpp
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include "communication/rpc/messages.hpp"
+#include "distributed/serialization.hpp"
+#include "distributed/index_rpc_messages.capnp.h"
+cpp<#
+
+(lcp:namespace distributed)
+
+(lcp:capnp-namespace "distributed")
+
+(lcp:capnp-import 'storage "/storage/serialization.capnp")
+
+(lcp:define-rpc build-index
+    (:request
+     ((label "storage::Label" :capnp-type "Storage.Common")
+      (property "storage::Property" :capnp-type "Storage.Common")
+      (tx-id "tx::TransactionId" :capnp-type "UInt64")))
+  (:response ()))
+
+(lcp:pop-namespace) ;; distributed
diff --git a/src/distributed/index_rpc_server.cpp b/src/distributed/index_rpc_server.cpp
index 6964ebcc6..a88b0595d 100644
--- a/src/distributed/index_rpc_server.cpp
+++ b/src/distributed/index_rpc_server.cpp
@@ -7,27 +7,27 @@ namespace distributed {
 IndexRpcServer::IndexRpcServer(database::GraphDb &db,
                                communication::rpc::Server &server)
     : db_(db), rpc_server_(server) {
-  rpc_server_.Register<BuildIndexRpc>([this](const BuildIndexReq &req) {
+  rpc_server_.Register<BuildIndexRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        BuildIndexReq req;
+        req.Load(req_reader);
+        database::LabelPropertyIndex::Key key{req.label, req.property};
+        database::GraphDbAccessor dba(db_, req.tx_id);
 
-    database::LabelPropertyIndex::Key key{req.member.label,
-                                          req.member.property};
-    database::GraphDbAccessor dba(db_, req.member.tx_id);
-
-    if (db_.storage().label_property_index_.CreateIndex(key) == false) {
-      // If we are a distributed worker we just have to wait till the index
-      // (which should be in progress of being created) is created so that our
-      // return guarantess that the index has been built - this assumes that
-      // no worker thread that is creating an index will fail
-      while (!dba.LabelPropertyIndexExists(key.label_, key.property_)) {
-        // TODO reconsider this constant, currently rule-of-thumb chosen
-        std::this_thread::sleep_for(std::chrono::microseconds(100));
-      }
-    } else {
-      dba.PopulateIndex(key);
-      dba.EnableIndex(key);
-    }
-    return std::make_unique<BuildIndexRes>();
-  });
+        if (db_.storage().label_property_index_.CreateIndex(key) == false) {
+          // If we are a distributed worker we just have to wait till the index
+          // (which should be in progress of being created) is created so that
+          // our return guarantess that the index has been built - this assumes
+          // that no worker thread that is creating an index will fail
+          while (!dba.LabelPropertyIndexExists(key.label_, key.property_)) {
+            // TODO reconsider this constant, currently rule-of-thumb chosen
+            std::this_thread::sleep_for(std::chrono::microseconds(100));
+          }
+        } else {
+          dba.PopulateIndex(key);
+          dba.EnableIndex(key);
+        }
+      });
 }
 
 }  // namespace distributed
diff --git a/src/distributed/plan_consumer.cpp b/src/distributed/plan_consumer.cpp
index 9e83b5785..fa48f2ce2 100644
--- a/src/distributed/plan_consumer.cpp
+++ b/src/distributed/plan_consumer.cpp
@@ -4,19 +4,21 @@ namespace distributed {
 
 PlanConsumer::PlanConsumer(communication::rpc::Server &server)
     : server_(server) {
-  server_.Register<DistributedPlanRpc>([this](const DispatchPlanReq &req) {
-    plan_cache_.access().insert(
-        req.plan_id_,
-        std::make_unique<PlanPack>(
-            req.plan_, req.symbol_table_,
-            std::move(const_cast<DispatchPlanReq &>(req).storage_)));
-    return std::make_unique<DispatchPlanRes>();
-  });
+  server_.Register<DispatchPlanRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        DispatchPlanReq req;
+        req.Load(req_reader);
+        plan_cache_.access().insert(
+            req.plan_id, std::make_unique<PlanPack>(req.plan, req.symbol_table,
+                                                    std::move(req.storage)));
+        DispatchPlanRes res;
+        res.Save(res_builder);
+      });
 
-  server_.Register<RemovePlanRpc>([this](const RemovePlanReq &req) {
-    plan_cache_.access().remove(req.member);
-    return std::make_unique<RemovePlanRes>();
-  });
+  server_.Register<RemovePlanRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        plan_cache_.access().remove(req_reader.getMember());
+      });
 }
 
 PlanConsumer::PlanPack &PlanConsumer::PlanForId(int64_t plan_id) const {
diff --git a/src/distributed/plan_consumer.hpp b/src/distributed/plan_consumer.hpp
index 0155805e4..933e50a99 100644
--- a/src/distributed/plan_consumer.hpp
+++ b/src/distributed/plan_consumer.hpp
@@ -16,14 +16,14 @@ class PlanConsumer {
  public:
   struct PlanPack {
     PlanPack(std::shared_ptr<query::plan::LogicalOperator> plan,
-             SymbolTable symbol_table, AstTreeStorage storage)
+             query::SymbolTable symbol_table, query::AstTreeStorage storage)
         : plan(plan),
           symbol_table(std::move(symbol_table)),
           storage(std::move(storage)) {}
 
     std::shared_ptr<query::plan::LogicalOperator> plan;
-    SymbolTable symbol_table;
-    const AstTreeStorage storage;
+    query::SymbolTable symbol_table;
+    const query::AstTreeStorage storage;
   };
 
   explicit PlanConsumer(communication::rpc::Server &server);
diff --git a/src/distributed/plan_dispatcher.cpp b/src/distributed/plan_dispatcher.cpp
index 72ae13418..bd1b34429 100644
--- a/src/distributed/plan_dispatcher.cpp
+++ b/src/distributed/plan_dispatcher.cpp
@@ -6,13 +6,13 @@ PlanDispatcher::PlanDispatcher(RpcWorkerClients &clients) : clients_(clients) {}
 
 void PlanDispatcher::DispatchPlan(
     int64_t plan_id, std::shared_ptr<query::plan::LogicalOperator> plan,
-    const SymbolTable &symbol_table) {
+    const query::SymbolTable &symbol_table) {
   auto futures = clients_.ExecuteOnWorkers<void>(
       0, [plan_id, plan, symbol_table](
              int worker_id, communication::rpc::ClientPool &client_pool) {
         auto result =
-            client_pool.Call<DistributedPlanRpc>(plan_id, plan, symbol_table);
-        CHECK(result) << "DistributedPlanRpc failed";
+            client_pool.Call<DispatchPlanRpc>(plan_id, plan, symbol_table);
+        CHECK(result) << "DispatchPlanRpc failed";
       });
 
   for (auto &future : futures) {
diff --git a/src/distributed/plan_dispatcher.hpp b/src/distributed/plan_dispatcher.hpp
index 9e2105b31..c8763f7e3 100644
--- a/src/distributed/plan_dispatcher.hpp
+++ b/src/distributed/plan_dispatcher.hpp
@@ -18,7 +18,7 @@ class PlanDispatcher {
   /** Dispatch a plan to all workers and wait for their acknowledgement. */
   void DispatchPlan(int64_t plan_id,
                     std::shared_ptr<query::plan::LogicalOperator> plan,
-                    const SymbolTable &symbol_table);
+                    const query::SymbolTable &symbol_table);
 
   /** Remove a plan from all workers and wait for their acknowledgement. */
   void RemovePlan(int64_t plan_id);
diff --git a/src/distributed/plan_rpc_messages.hpp b/src/distributed/plan_rpc_messages.hpp
deleted file mode 100644
index 506365481..000000000
--- a/src/distributed/plan_rpc_messages.hpp
+++ /dev/null
@@ -1,63 +0,0 @@
-#pragma once
-
-#include "boost/serialization/access.hpp"
-#include "boost/serialization/base_object.hpp"
-
-#include "communication/rpc/messages.hpp"
-#include "query/frontend/ast/ast.hpp"
-#include "query/frontend/semantic/symbol_table.hpp"
-#include "query/plan/operator.hpp"
-
-namespace distributed {
-
-using communication::rpc::Message;
-using SymbolTable = query::SymbolTable;
-using AstTreeStorage = query::AstTreeStorage;
-
-struct DispatchPlanReq : public Message {
-  DispatchPlanReq() {}
-  DispatchPlanReq(int64_t plan_id,
-                  std::shared_ptr<query::plan::LogicalOperator> plan,
-                  SymbolTable symbol_table)
-
-      : plan_id_(plan_id), plan_(plan), symbol_table_(symbol_table) {}
-  int64_t plan_id_;
-  std::shared_ptr<query::plan::LogicalOperator> plan_;
-  SymbolTable symbol_table_;
-  AstTreeStorage storage_;
-
- private:
-  friend class boost::serialization::access;
-
-  BOOST_SERIALIZATION_SPLIT_MEMBER();
-
-  template <class TArchive>
-  void save(TArchive &ar, const unsigned int) const {
-    ar &boost::serialization::base_object<Message>(*this);
-    ar &plan_id_;
-    ar &plan_;
-    ar &symbol_table_;
-  }
-
-  template <class TArchive>
-  void load(TArchive &ar, const unsigned int) {
-    ar &boost::serialization::base_object<Message>(*this);
-    ar &plan_id_;
-    ar &plan_;
-    ar &symbol_table_;
-    storage_ = std::move(
-        ar.template get_helper<AstTreeStorage>(AstTreeStorage::kHelperId));
-  }
-};
-
-RPC_NO_MEMBER_MESSAGE(DispatchPlanRes);
-
-using DistributedPlanRpc =
-    communication::rpc::RequestResponse<DispatchPlanReq, DispatchPlanRes>;
-
-RPC_SINGLE_MEMBER_MESSAGE(RemovePlanReq, int64_t);
-RPC_NO_MEMBER_MESSAGE(RemovePlanRes);
-using RemovePlanRpc =
-    communication::rpc::RequestResponse<RemovePlanReq, RemovePlanRes>;
-
-}  // namespace distributed
diff --git a/src/distributed/plan_rpc_messages.lcp b/src/distributed/plan_rpc_messages.lcp
new file mode 100644
index 000000000..b55227308
--- /dev/null
+++ b/src/distributed/plan_rpc_messages.lcp
@@ -0,0 +1,59 @@
+#>cpp
+#pragma once
+
+#include "communication/rpc/messages.hpp"
+#include "query/frontend/ast/ast.hpp"
+#include "query/frontend/semantic/symbol_table.hpp"
+#include "query/plan/operator.hpp"
+
+#include "distributed/plan_rpc_messages.capnp.h"
+cpp<#
+
+(lcp:namespace distributed)
+
+(lcp:capnp-namespace "distributed")
+
+(lcp:capnp-import 'utils "/utils/serialization.capnp")
+(lcp:capnp-import 'plan "/query/plan/operator.capnp")
+(lcp:capnp-import 'sem "/query/frontend/semantic/symbol.capnp")
+
+(defun load-plan (reader member)
+  #>cpp
+  query::plan::LogicalOperator::LoadHelper helper;
+  ${member} = utils::LoadSharedPtr<query::plan::capnp::LogicalOperator, query::plan::LogicalOperator>(
+      ${reader}, [&helper](const auto &reader) {
+                   auto op = query::plan::LogicalOperator::Construct(reader);
+                   op->Load(reader, &helper);
+                   return op.release();
+                 }, &helper.loaded_ops);
+  storage = std::move(helper.ast_storage);
+  cpp<#)
+
+(defun save-plan (builder member)
+  #>cpp
+  query::plan::LogicalOperator::SaveHelper helper;
+  utils::SaveSharedPtr<query::plan::capnp::LogicalOperator, query::plan::LogicalOperator>(
+      ${member}, &${builder},
+      [&helper](auto *builder, const auto &val) {
+        val.Save(builder, &helper);
+      }, &helper.saved_ops);
+  cpp<#)
+
+(lcp:define-rpc dispatch-plan
+    (:request
+     ((plan-id :int64_t)
+      (plan "std::shared_ptr<query::plan::LogicalOperator>"
+            :capnp-type "Utils.SharedPtr(Plan.LogicalOperator)"
+            :capnp-save #'save-plan :capnp-load #'load-plan)
+      (symbol-table "query::SymbolTable" :capnp-type "Sem.SymbolTable")
+      (storage "query::AstTreeStorage" :initarg nil
+               :save-fun ""
+               :load-fun "storage = std::move(ar.template get_helper<query::AstTreeStorage>(query::AstTreeStorage::kHelperId));"
+               :capnp-save :dont-save)))
+  (:response ()))
+
+(lcp:define-rpc remove-plan
+    (:request ((member :int64_t)))
+  (:response ()))
+
+(lcp:pop-namespace) ;; distributed
diff --git a/src/distributed/produce_rpc_server.cpp b/src/distributed/produce_rpc_server.cpp
index 1b3d3a2e7..466bbe30e 100644
--- a/src/distributed/produce_rpc_server.cpp
+++ b/src/distributed/produce_rpc_server.cpp
@@ -96,15 +96,22 @@ ProduceRpcServer::ProduceRpcServer(
       produce_rpc_server_(server),
       plan_consumer_(plan_consumer),
       tx_engine_(tx_engine) {
-  produce_rpc_server_.Register<PullRpc>([this](const PullReq &req) {
-    return std::make_unique<PullRes>(Pull(req));
-  });
+  produce_rpc_server_.Register<PullRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        PullReq req;
+        req.Load(req_reader);
+        PullRes res(Pull(req));
+        res.Save(res_builder);
+      });
 
   produce_rpc_server_.Register<TransactionCommandAdvancedRpc>(
-      [this](const TransactionCommandAdvancedReq &req) {
+      [this](const auto &req_reader, auto *res_builder) {
+        TransactionCommandAdvancedReq req;
+        req.Load(req_reader);
         tx_engine_.UpdateCommand(req.member);
         db_.data_manager().ClearCacheForSingleTransaction(req.member);
-        return std::make_unique<TransactionCommandAdvancedRes>();
+        TransactionCommandAdvancedRes res;
+        res.Save(res_builder);
       });
 }
 
@@ -145,22 +152,22 @@ ProduceRpcServer::OngoingProduce &ProduceRpcServer::GetOngoingProduce(
 PullResData ProduceRpcServer::Pull(const PullReq &req) {
   auto &ongoing_produce = GetOngoingProduce(req);
 
-  PullResData result{db_.WorkerId(), req.send_old, req.send_new};
-  result.state_and_frames.pull_state = PullState::CURSOR_IN_PROGRESS;
+  PullResData result(db_.WorkerId(), req.send_old, req.send_new);
+  result.pull_state = PullState::CURSOR_IN_PROGRESS;
 
   if (req.accumulate) {
-    result.state_and_frames.pull_state = ongoing_produce.Accumulate();
+    result.pull_state = ongoing_produce.Accumulate();
     // If an error ocurred, we need to return that error.
-    if (result.state_and_frames.pull_state != PullState::CURSOR_EXHAUSTED) {
+    if (result.pull_state != PullState::CURSOR_EXHAUSTED) {
       return result;
     }
   }
 
   for (int i = 0; i < req.batch_size; ++i) {
     auto pull_result = ongoing_produce.Pull();
-    result.state_and_frames.pull_state = pull_result.second;
+    result.pull_state = pull_result.second;
     if (pull_result.second != PullState::CURSOR_IN_PROGRESS) break;
-    result.state_and_frames.frames.emplace_back(std::move(pull_result.first));
+    result.frames.emplace_back(std::move(pull_result.first));
   }
 
   return result;
diff --git a/src/distributed/pull_produce_rpc_messages.hpp b/src/distributed/pull_produce_rpc_messages.hpp
deleted file mode 100644
index e2f41ff83..000000000
--- a/src/distributed/pull_produce_rpc_messages.hpp
+++ /dev/null
@@ -1,381 +0,0 @@
-#pragma once
-
-#include <cstdint>
-#include <functional>
-#include <string>
-
-#include "boost/serialization/utility.hpp"
-#include "boost/serialization/vector.hpp"
-
-#include "communication/rpc/messages.hpp"
-#include "distributed/serialization.hpp"
-#include "query/frontend/semantic/symbol.hpp"
-#include "query/parameters.hpp"
-#include "storage/address_types.hpp"
-#include "transactions/type.hpp"
-#include "utils/serialization.hpp"
-
-namespace distributed {
-
-/// The default number of results returned via RPC from remote execution to the
-/// master that requested it.
-constexpr int kDefaultBatchSize = 20;
-
-/// Returnd along with a batch of results in the remote-pull RPC. Indicates the
-/// state of execution on the worker.
-enum class PullState {
-  CURSOR_EXHAUSTED,
-  CURSOR_IN_PROGRESS,
-  SERIALIZATION_ERROR,
-  LOCK_TIMEOUT_ERROR,
-  UPDATE_DELETED_ERROR,
-  RECONSTRUCTION_ERROR,
-  UNABLE_TO_DELETE_VERTEX_ERROR,
-  HINTED_ABORT_ERROR,
-  QUERY_ERROR
-};
-
-struct PullReq : public communication::rpc::Message {
-  PullReq() {}
-  PullReq(tx::TransactionId tx_id, tx::Snapshot tx_snapshot, int64_t plan_id,
-          tx::CommandId command_id, const Parameters &params,
-          std::vector<query::Symbol> symbols, bool accumulate, int batch_size,
-          bool send_old, bool send_new)
-      : tx_id(tx_id),
-        tx_snapshot(tx_snapshot),
-        plan_id(plan_id),
-        command_id(command_id),
-        params(params),
-        symbols(symbols),
-        accumulate(accumulate),
-        batch_size(batch_size),
-        send_old(send_old),
-        send_new(send_new) {}
-
-  tx::TransactionId tx_id;
-  tx::Snapshot tx_snapshot;
-  int64_t plan_id;
-  tx::CommandId command_id;
-  Parameters params;
-  std::vector<query::Symbol> symbols;
-  bool accumulate;
-  int batch_size;
-  // Indicates which of (old, new) records of a graph element should be sent.
-  bool send_old;
-  bool send_new;
-
- private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void save(TArchive &ar, unsigned int) const {
-    ar << boost::serialization::base_object<communication::rpc::Message>(*this);
-    ar << tx_id;
-    ar << tx_snapshot;
-    ar << plan_id;
-    ar << command_id;
-    ar << params.size();
-    for (auto &kv : params) {
-      ar << kv.first;
-      // Params never contain a vertex/edge, so save plan TypedValue.
-      utils::SaveTypedValue(ar, kv.second);
-    }
-    ar << symbols;
-    ar << accumulate;
-    ar << batch_size;
-    ar << send_old;
-    ar << send_new;
-  }
-
-  template <class TArchive>
-  void load(TArchive &ar, unsigned int) {
-    ar >> boost::serialization::base_object<communication::rpc::Message>(*this);
-    ar >> tx_id;
-    ar >> tx_snapshot;
-    ar >> plan_id;
-    ar >> command_id;
-    size_t params_size;
-    ar >> params_size;
-    for (size_t i = 0; i < params_size; ++i) {
-      int token_pos;
-      ar >> token_pos;
-      query::TypedValue param;
-      // Params never contain a vertex/edge, so load plan TypedValue.
-      utils::LoadTypedValue(ar, param);
-      params.Add(token_pos, param);
-    }
-    ar >> symbols;
-    ar >> accumulate;
-    ar >> batch_size;
-    ar >> send_old;
-    ar >> send_new;
-  }
-  BOOST_SERIALIZATION_SPLIT_MEMBER()
-};
-
-/// The data returned to the end consumer (the Pull operator). Contains
-/// only the relevant parts of the response, ready for use.
-struct PullData {
-  PullState pull_state;
-  std::vector<std::vector<query::TypedValue>> frames;
-};
-
-/// The data of the remote pull response. Post-processing is required after
-/// deserialization to initialize Vertex/Edge typed values in the frames
-/// (possibly encapsulated in lists/maps) to their proper values. This requires
-/// a GraphDbAccessor and therefore can't be done as part of deserialization.
-///
-/// TODO - make it possible to inject a &GraphDbAcessor from the Pull
-/// layer
-/// all the way into RPC data deserialization to remove the requirement for
-/// post-processing. The current approach of holding references to parts of the
-/// frame (potentially embedded in lists/maps) is too error-prone.
-struct PullResData {
- private:
-  // Temp cache for deserialized vertices and edges. These objects are created
-  // during deserialization. They are used immediatelly after during
-  // post-processing. The vertex/edge data ownership gets transfered to the
-  // Cache, and the `element_in_frame` reference is used to set the
-  // appropriate accessor to the appropriate value. Not used on side that
-  // generates the response.
-  template <typename TRecord>
-  struct GraphElementData {
-    using AddressT = storage::Address<mvcc::VersionList<TRecord>>;
-    using PtrT = std::unique_ptr<TRecord>;
-
-    GraphElementData(AddressT address, PtrT old_record, PtrT new_record,
-                     query::TypedValue *element_in_frame)
-        : global_address(address),
-          old_record(std::move(old_record)),
-          new_record(std::move(new_record)),
-          element_in_frame(element_in_frame) {}
-
-    storage::Address<mvcc::VersionList<TRecord>> global_address;
-    std::unique_ptr<TRecord> old_record;
-    std::unique_ptr<TRecord> new_record;
-    // The position in frame is optional. This same structure is used for
-    // deserializing path elements, in which case the vertex/edge in question is
-    // not directly part of the frame.
-    query::TypedValue *element_in_frame;
-  };
-
-  // Same like `GraphElementData`, but for paths.
-  struct PathData {
-    PathData(query::TypedValue &path_in_frame) : path_in_frame(path_in_frame) {}
-    std::vector<GraphElementData<Vertex>> vertices;
-    std::vector<GraphElementData<Edge>> edges;
-    query::TypedValue &path_in_frame;
-  };
-
- public:
-  PullResData() {}  // Default constructor required for serialization.
-  PullResData(int worker_id, bool send_old, bool send_new)
-      : worker_id(worker_id), send_old(send_old), send_new(send_new) {}
-
-  PullResData(const PullResData &) = delete;
-  PullResData &operator=(const PullResData &) = delete;
-  PullResData(PullResData &&) = default;
-  PullResData &operator=(PullResData &&) = default;
-
-  PullData state_and_frames;
-  // Id of the worker on which the response is created, used for serializing
-  // vertices (converting local to global addresses).
-  int worker_id;
-  // Indicates which of (old, new) records of a graph element should be sent.
-  bool send_old;
-  bool send_new;
-
-  // Temporary caches used between deserialization and post-processing
-  // (transfering the ownership of this data to a Cache).
-  std::vector<GraphElementData<Vertex>> vertices;
-  std::vector<GraphElementData<Edge>> edges;
-  std::vector<PathData> paths;
-
-  /// Saves a typed value that is a vertex/edge/path.
-  template <class TArchive>
-  void SaveGraphElement(TArchive &ar, const query::TypedValue &value) const {
-    // Helper template function for storing a vertex or an edge.
-    auto save_element = [&ar, this](auto element_accessor) {
-      ar << element_accessor.GlobalAddress().raw();
-
-      // If both old and new are null, we need to reconstruct.
-      if (!(element_accessor.GetOld() || element_accessor.GetNew())) {
-        bool result = element_accessor.Reconstruct();
-        CHECK(result) << "Attempting to serialize an element not visible to "
-                         "current transaction.";
-      }
-      auto *old_rec = element_accessor.GetOld();
-      if (send_old && old_rec) {
-        ar << true;
-        distributed::SaveElement(ar, *old_rec, worker_id);
-      } else {
-        ar << false;
-      }
-      if (send_new) {
-        // Must call SwitchNew as that will trigger a potentially necesary
-        // Reconstruct.
-        element_accessor.SwitchNew();
-        auto *new_rec = element_accessor.GetNew();
-        if (new_rec) {
-          ar << true;
-          distributed::SaveElement(ar, *new_rec, worker_id);
-        } else {
-          ar << false;
-        }
-      } else {
-        ar << false;
-      }
-    };
-    switch (value.type()) {
-      case query::TypedValue::Type::Vertex:
-        save_element(value.ValueVertex());
-        break;
-      case query::TypedValue::Type::Edge:
-        save_element(value.ValueEdge());
-        break;
-      case query::TypedValue::Type::Path: {
-        auto &path = value.ValuePath();
-        ar << path.size();
-        save_element(path.vertices()[0]);
-        for (size_t i = 0; i < path.size(); ++i) {
-          save_element(path.edges()[i]);
-          save_element(path.vertices()[i + 1]);
-        }
-        break;
-      }
-      default:
-        LOG(FATAL) << "Unsupported graph element type: " << value.type();
-    }
-  }
-
-  /// Loads a typed value that is a vertex/edge/path. Part of the
-  /// deserialization process, populates the temporary data caches which are
-  /// processed later.
-  template <class TArchive>
-  void LoadGraphElement(TArchive &ar, query::TypedValue::Type type,
-                        query::TypedValue &value) {
-    auto load_edge = [](auto &ar) {
-      bool exists;
-      ar >> exists;
-      return exists ? LoadEdge(ar) : nullptr;
-    };
-    auto load_vertex = [](auto &ar) {
-      bool exists;
-      ar >> exists;
-      return exists ? LoadVertex(ar) : nullptr;
-    };
-
-    switch (type) {
-      case query::TypedValue::Type::Vertex: {
-        storage::VertexAddress::StorageT address;
-        ar >> address;
-        vertices.emplace_back(storage::VertexAddress(address), load_vertex(ar),
-                              load_vertex(ar), &value);
-        break;
-      }
-      case query::TypedValue::Type::Edge: {
-        storage::VertexAddress::StorageT address;
-        ar >> address;
-        edges.emplace_back(storage::EdgeAddress(address), load_edge(ar),
-                           load_edge(ar), &value);
-        break;
-      }
-      case query::TypedValue::Type::Path: {
-        size_t path_size;
-        ar >> path_size;
-
-        paths.emplace_back(value);
-        auto &path_data = paths.back();
-
-        storage::VertexAddress::StorageT vertex_address;
-        storage::EdgeAddress::StorageT edge_address;
-        ar >> vertex_address;
-        path_data.vertices.emplace_back(storage::VertexAddress(vertex_address),
-                                        load_vertex(ar), load_vertex(ar),
-                                        nullptr);
-        for (size_t i = 0; i < path_size; ++i) {
-          ar >> edge_address;
-          path_data.edges.emplace_back(storage::EdgeAddress(edge_address),
-                                       load_edge(ar), load_edge(ar), nullptr);
-          ar >> vertex_address;
-          path_data.vertices.emplace_back(
-              storage::VertexAddress(vertex_address), load_vertex(ar),
-              load_vertex(ar), nullptr);
-        }
-        break;
-      }
-      default:
-        LOG(FATAL) << "Unsupported graph element type: " << type;
-    }
-  }
-};
-
-class PullRes : public communication::rpc::Message {
- public:
-  PullRes() {}
-  PullRes(PullResData data) : data(std::move(data)) {}
-
-  PullResData data;
-
- private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void save(TArchive &ar, unsigned int) const {
-    ar << boost::serialization::base_object<communication::rpc::Message>(*this);
-    ar << data.state_and_frames.pull_state;
-    ar << data.state_and_frames.frames.size();
-    // We need to indicate how many values are in each frame.
-    // Assume all the frames have an equal number of elements.
-    ar << (data.state_and_frames.frames.size() == 0
-               ? 0
-               : data.state_and_frames.frames[0].size());
-    for (const auto &frame : data.state_and_frames.frames)
-      for (const auto &value : frame) {
-        utils::SaveTypedValue<TArchive>(
-            ar, value, [this](TArchive &ar, const query::TypedValue &value) {
-              data.SaveGraphElement(ar, value);
-            });
-      }
-  }
-
-  template <class TArchive>
-  void load(TArchive &ar, unsigned int) {
-    ar >> boost::serialization::base_object<communication::rpc::Message>(*this);
-    ar >> data.state_and_frames.pull_state;
-    size_t frame_count;
-    ar >> frame_count;
-    data.state_and_frames.frames.reserve(frame_count);
-    size_t frame_size;
-    ar >> frame_size;
-    for (size_t i = 0; i < frame_count; ++i) {
-      data.state_and_frames.frames.emplace_back();
-      auto &current_frame = data.state_and_frames.frames.back();
-      current_frame.reserve(frame_size);
-      for (size_t j = 0; j < frame_size; ++j) {
-        current_frame.emplace_back();
-        utils::LoadTypedValue<TArchive>(
-            ar, current_frame.back(),
-            [this](TArchive &ar, query::TypedValue::TypedValue::Type type,
-                   query::TypedValue &value) {
-              data.LoadGraphElement(ar, type, value);
-            });
-      }
-    }
-  }
-  BOOST_SERIALIZATION_SPLIT_MEMBER()
-};
-
-using PullRpc = communication::rpc::RequestResponse<PullReq, PullRes>;
-
-// TODO make a separate RPC for the continuation of an existing pull, as an
-// optimization not to have to send the full PullReqData pack every
-// time.
-
-RPC_SINGLE_MEMBER_MESSAGE(TransactionCommandAdvancedReq, tx::TransactionId);
-RPC_NO_MEMBER_MESSAGE(TransactionCommandAdvancedRes);
-using TransactionCommandAdvancedRpc =
-    communication::rpc::RequestResponse<TransactionCommandAdvancedReq,
-                                        TransactionCommandAdvancedRes>;
-
-}  // namespace distributed
diff --git a/src/distributed/pull_produce_rpc_messages.lcp b/src/distributed/pull_produce_rpc_messages.lcp
new file mode 100644
index 000000000..849121140
--- /dev/null
+++ b/src/distributed/pull_produce_rpc_messages.lcp
@@ -0,0 +1,547 @@
+#>cpp
+#pragma once
+
+#include <cstdint>
+#include <functional>
+#include <string>
+
+#include "communication/rpc/messages.hpp"
+#include "distributed/pull_produce_rpc_messages.capnp.h"
+#include "distributed/serialization.hpp"
+#include "query/frontend/semantic/symbol.hpp"
+#include "query/parameters.hpp"
+#include "storage/address_types.hpp"
+#include "transactions/type.hpp"
+#include "utils/serialization.hpp"
+cpp<#
+
+(lcp:in-impl
+ #>cpp
+ #include "database/graph_db_accessor.hpp"
+ #include "distributed/data_manager.hpp"
+ cpp<#)
+
+(lcp:namespace distributed)
+
+(lcp:capnp-namespace "distributed")
+
+(lcp:capnp-import 'dis "/distributed/serialization.capnp")
+(lcp:capnp-import 'sem "/query/frontend/semantic/symbol.capnp")
+(lcp:capnp-import 'tx "/transactions/common.capnp")
+(lcp:capnp-import 'utils "/utils/serialization.capnp")
+
+(lcp:capnp-type-conversion "tx::CommandId" "UInt32")
+(lcp:capnp-type-conversion "tx::Snapshot" "Tx.Snapshot")
+(lcp:capnp-type-conversion "tx::TransactionId" "UInt64")
+
+#>cpp
+/// The default number of results returned via RPC from remote execution to the
+/// master that requested it.
+constexpr int kDefaultBatchSize = 20;
+cpp<#
+
+(lcp:define-enum pull-state
+    (cursor-exhausted
+     cursor-in-progress
+     serialization-error
+     lock-timeout-error
+     update-deleted-error
+     reconstruction-error
+     unable-to-delete-vertex-error
+     hinted-abort-error
+     query-error)
+  (:documentation "Returned along with a batch of results in the remote-pull
+RPC. Indicates the state of execution on the worker.")
+  (:serialize))
+
+(lcp:define-struct pull-data ()
+  ((pull-state "PullState")
+   (frames "std::vector<std::vector<query::TypedValue>>"))
+  (:documentation
+   "The data returned to the end consumer (the Pull operator). Contains only
+the relevant parts of the response, ready for use."))
+
+(lcp:define-struct pull-res-data ()
+  ((pull-state "PullState"
+               :capnp-init nil
+               :capnp-save (lcp:capnp-save-enum "capnp::PullState" "PullState")
+               :capnp-load (lcp:capnp-load-enum "capnp::PullState" "PullState"))
+   (frames "std::vector<std::vector<query::TypedValue>>"
+           :capnp-type "List(List(Dis.TypedValue))"
+           :capnp-save
+           (lambda (builder member)
+             #>cpp
+             for (size_t frame_i = 0; frame_i < ${member}.size(); ++frame_i) {
+               const auto &frame = ${member}[frame_i];
+               auto frame_builder = ${builder}.init(frame_i, frame.size());
+               for (size_t val_i = 0; val_i < frame.size(); ++val_i) {
+                 const auto &value = frame[val_i];
+                 auto value_builder = frame_builder[val_i];
+                 utils::SaveCapnpTypedValue(
+                     value, &value_builder,
+                     [this](const auto &value, auto *builder) {
+                       this->SaveGraphElement(value, builder);
+                     });
+               }
+             }
+             cpp<#)
+           :capnp-load
+           (lambda (reader member)
+             #>cpp
+             ${member}.reserve(${reader}.size());
+             for (const auto &frame_reader : ${reader}) {
+               std::vector<query::TypedValue> current_frame;
+               current_frame.reserve(frame_reader.size());
+               for (const auto &value_reader : frame_reader) {
+                 query::TypedValue value;
+                 utils::LoadCapnpTypedValue(
+                     value_reader, &value,
+                     [this, dba](const auto &reader, auto *value) {
+                       this->LoadGraphElement(dba, reader, value);
+                     });
+                 current_frame.emplace_back(value);
+               }
+               ${member}.emplace_back(current_frame);
+             }
+             cpp<#))
+   (worker-id :int16_t :capnp-save :dont-save
+              :documentation
+              "Id of the worker on which the response is created, used for
+serializing vertices (converting local to global addresses). Indicates which
+of (old, new) records of a graph element should be sent.")
+   (send-old :bool :capnp-save :dont-save)
+   (send-new :bool :capnp-save :dont-save)
+   ;; Temporary caches used between deserialization and post-processing
+   ;; (transfering the ownership of this data to a Cache).
+   (vertices "std::vector<GraphElementData<Vertex>>" :capnp-save :dont-save)
+   (edges "std::vector<GraphElementData<Edge>>" :capnp-save :dont-save)
+   (paths "std::vector<PathData>" :capnp-save :dont-save))
+  (:documentation
+   "The data of the remote pull response. Post-processing is required after
+deserialization to initialize Vertex/Edge typed values in the frames (possibly
+encapsulated in lists/maps) to their proper values. This requires a
+GraphDbAccessor and therefore can't be done as part of deserialization.
+
+TODO - make it possible to inject a &GraphDbAcessor from the Pull layer all
+the way into RPC data deserialization to remove the requirement for
+post-processing. The current approach of holding references to parts of the
+frame (potentially embedded in lists/maps) is too error-prone.")
+  (:public
+   #>cpp
+   private:
+   cpp<#
+   (lcp:define-struct (graph-element-data t-record) ()
+     ((global-address "storage::Address<mvcc::VersionList<TRecord>>")
+      (old-record "std::unique_ptr<TRecord>")
+      (new-record "std::unique_ptr<TRecord>")
+      (element-in-frame
+       "query::TypedValue *"
+       :documentation
+       "The position in frame is optional. This same structure is used for
+deserializing path elements, in which case the vertex/edge in question is not
+directly part of the frame."))
+     (:documentation
+      "Temp cache for deserialized vertices and edges. These objects are
+created during deserialization. They are used immediatelly after during
+post-processing. The vertex/edge data ownership gets transfered to the Cache,
+and the `element_in_frame` reference is used to set the appropriate accessor
+to the appropriate value. Not used on side that generates the response.")
+     (:public
+      #>cpp
+      GraphElementData(storage::Address<mvcc::VersionList<TRecord>> address,
+                       std::unique_ptr<TRecord> old_record, std::unique_ptr<TRecord> new_record,
+                       query::TypedValue *element_in_frame)
+        : global_address(address),
+          old_record(std::move(old_record)),
+          new_record(std::move(new_record)),
+          element_in_frame(element_in_frame) {}
+      cpp<#))
+   (lcp:define-struct path-data ()
+     ((vertices "std::vector<GraphElementData<Vertex>>")
+      (edges "std::vector<GraphElementData<Edge>>")
+      (path-in-frame "query::TypedValue *"))
+     (:public
+      #>cpp
+      PathData(query::TypedValue *path_in_frame) : path_in_frame(path_in_frame) {}
+      cpp<#)
+     (:documentation "Same like `GraphElementData`, but for paths."))
+   #>cpp
+   public:
+    PullResData() {}  // Default constructor required for serialization.
+    PullResData(int worker_id, bool send_old, bool send_new)
+        : worker_id(worker_id), send_old(send_old), send_new(send_new) {}
+
+    PullResData(const PullResData &) = delete;
+    PullResData &operator=(const PullResData &) = delete;
+    PullResData(PullResData &&) = default;
+    PullResData &operator=(PullResData &&) = default;
+
+
+    /// Saves a typed value that is a vertex/edge/path.
+    template <class TArchive>
+    void SaveGraphElement(TArchive &ar, const query::TypedValue &value) const {
+      // Helper template function for storing a vertex or an edge.
+      auto save_element = [&ar, this](auto element_accessor) {
+        ar << element_accessor.GlobalAddress().raw();
+
+        // If both old and new are null, we need to reconstruct.
+        if (!(element_accessor.GetOld() || element_accessor.GetNew())) {
+          bool result = element_accessor.Reconstruct();
+          CHECK(result) << "Attempting to serialize an element not visible to "
+                           "current transaction.";
+        }
+        auto *old_rec = element_accessor.GetOld();
+        if (send_old && old_rec) {
+          ar << true;
+          distributed::SaveElement(ar, *old_rec, worker_id);
+        } else {
+          ar << false;
+        }
+        if (send_new) {
+          // Must call SwitchNew as that will trigger a potentially necesary
+          // Reconstruct.
+          element_accessor.SwitchNew();
+          auto *new_rec = element_accessor.GetNew();
+          if (new_rec) {
+            ar << true;
+            distributed::SaveElement(ar, *new_rec, worker_id);
+          } else {
+            ar << false;
+          }
+        } else {
+          ar << false;
+        }
+      };
+      switch (value.type()) {
+        case query::TypedValue::Type::Vertex:
+          save_element(value.ValueVertex());
+          break;
+        case query::TypedValue::Type::Edge:
+          save_element(value.ValueEdge());
+          break;
+        case query::TypedValue::Type::Path: {
+          auto &path = value.ValuePath();
+          ar << path.size();
+          save_element(path.vertices()[0]);
+          for (size_t i = 0; i < path.size(); ++i) {
+            save_element(path.edges()[i]);
+            save_element(path.vertices()[i + 1]);
+          }
+          break;
+        }
+        default:
+          LOG(FATAL) << "Unsupported graph element type: " << value.type();
+      }
+    }
+
+    /// Loads a typed value that is a vertex/edge/path. Part of the
+    /// deserialization process, populates the temporary data caches which are
+    /// processed later.
+    template <class TArchive>
+    void LoadGraphElement(TArchive &ar, query::TypedValue::Type type,
+                          query::TypedValue &value) {
+      auto load_edge = [](auto &ar) {
+        bool exists;
+        ar >> exists;
+        return exists ? LoadEdge(ar) : nullptr;
+      };
+      auto load_vertex = [](auto &ar) {
+        bool exists;
+        ar >> exists;
+        return exists ? LoadVertex(ar) : nullptr;
+      };
+
+      switch (type) {
+        case query::TypedValue::Type::Vertex: {
+          storage::VertexAddress::StorageT address;
+          ar >> address;
+          vertices.emplace_back(storage::VertexAddress(address), load_vertex(ar),
+                                load_vertex(ar), &value);
+          break;
+        }
+        case query::TypedValue::Type::Edge: {
+          storage::VertexAddress::StorageT address;
+          ar >> address;
+          edges.emplace_back(storage::EdgeAddress(address), load_edge(ar),
+                             load_edge(ar), &value);
+          break;
+        }
+        case query::TypedValue::Type::Path: {
+          size_t path_size;
+          ar >> path_size;
+
+          paths.emplace_back(&value);
+          auto &path_data = paths.back();
+
+          storage::VertexAddress::StorageT vertex_address;
+          storage::EdgeAddress::StorageT edge_address;
+          ar >> vertex_address;
+          path_data.vertices.emplace_back(storage::VertexAddress(vertex_address),
+                                          load_vertex(ar), load_vertex(ar),
+                                          nullptr);
+          for (size_t i = 0; i < path_size; ++i) {
+            ar >> edge_address;
+            path_data.edges.emplace_back(storage::EdgeAddress(edge_address),
+                                         load_edge(ar), load_edge(ar), nullptr);
+            ar >> vertex_address;
+            path_data.vertices.emplace_back(
+                storage::VertexAddress(vertex_address), load_vertex(ar),
+                load_vertex(ar), nullptr);
+          }
+          break;
+        }
+        default:
+          LOG(FATAL) << "Unsupported graph element type: " << type;
+      }
+    }
+   cpp<#)
+  (:private
+   #>cpp
+   void SaveGraphElement(const query::TypedValue &,
+                         distributed::capnp::TypedValue::Builder *) const;
+   void LoadGraphElement(database::GraphDbAccessor *,
+                         const distributed::capnp::TypedValue::Reader &,
+                         query::TypedValue *);
+   cpp<#)
+  (:serialize :capnp :load-args '((dba "database::GraphDbAccessor *"))))
+
+(lcp:in-impl
+ #>cpp
+ void PullResData::SaveGraphElement(
+     const query::TypedValue &value,
+     distributed::capnp::TypedValue::Builder *builder) const {
+   auto save_element = [this](auto accessor, auto *builder) {
+     builder->setAddress(accessor.GlobalAddress().raw());
+     // If both old and new are null, we need to reconstruct
+     if (!(accessor.GetOld() || accessor.GetNew())) {
+       bool result = accessor.Reconstruct();
+       CHECK(result) << "Attempting to serialize an element not visible to "
+                        "current transaction.";
+     }
+     auto *old_rec = accessor.GetOld();
+     if (send_old && old_rec) {
+       auto old_builder = builder->initOld();
+       distributed::SaveElement(*old_rec, &old_builder, worker_id);
+     }
+     if (send_new) {
+       // Must call SwitchNew as that will trigger a potentially necesary
+       // Reconstruct.
+       accessor.SwitchNew();
+       auto *new_rec = accessor.GetNew();
+       if (new_rec) {
+         auto new_builder = builder->initNew();
+         distributed::SaveElement(*new_rec, &new_builder, worker_id);
+       }
+     }
+   };
+   switch (value.type()) {
+     case query::TypedValue::Type::Vertex: {
+       auto vertex_builder = builder->initVertex();
+       save_element(value.ValueVertex(), &vertex_builder);
+       break;
+     }
+     case query::TypedValue::Type::Edge: {
+       auto edge_builder = builder->initEdge();
+       save_element(value.ValueEdge(), &edge_builder);
+       break;
+     }
+     case query::TypedValue::Type::Path: {
+       const auto &path = value.ValuePath();
+       auto path_builder = builder->initPath();
+       auto vertices_builder = path_builder.initVertices(path.vertices().size());
+       for (size_t i = 0; i < path.vertices().size(); ++i) {
+         auto vertex_builder = vertices_builder[i];
+         save_element(path.vertices()[i], &vertex_builder);
+       }
+       auto edges_builder = path_builder.initEdges(path.edges().size());
+       for (size_t i = 0; i < path.edges().size(); ++i) {
+         auto edge_builder = edges_builder[i];
+         save_element(path.edges()[i], &edge_builder);
+       }
+       break;
+     }
+     default:
+       LOG(FATAL) << "Unsupported graph element type: " << value.type();
+   }
+ }
+
+void PullResData::LoadGraphElement(
+    database::GraphDbAccessor *dba,
+    const distributed::capnp::TypedValue::Reader &reader,
+    query::TypedValue *value) {
+  auto load_vertex = [dba](const auto &vertex_reader) {
+    storage::VertexAddress global_address(vertex_reader.getAddress());
+    auto old_record =
+        vertex_reader.hasOld()
+            ? distributed::LoadVertex<const distributed::capnp::Vertex::Reader>(
+                  vertex_reader.getOld())
+            : nullptr;
+    auto new_record =
+        vertex_reader.hasNew()
+            ? distributed::LoadVertex<const distributed::capnp::Vertex::Reader>(
+                  vertex_reader.getNew())
+            : nullptr;
+    dba->db()
+        .data_manager()
+        .Elements<Vertex>(dba->transaction_id())
+        .emplace(global_address.gid(), std::move(old_record),
+                 std::move(new_record));
+    return VertexAccessor(global_address, *dba);
+  };
+  auto load_edge = [dba](const auto &edge_reader) {
+    storage::EdgeAddress global_address(edge_reader.getAddress());
+    auto old_record =
+        edge_reader.hasOld()
+            ? distributed::LoadEdge<const distributed::capnp::Edge::Reader>(
+                  edge_reader.getOld())
+            : nullptr;
+    auto new_record =
+        edge_reader.hasNew()
+            ? distributed::LoadEdge<const distributed::capnp::Edge::Reader>(
+                  edge_reader.getNew())
+            : nullptr;
+    dba->db()
+        .data_manager()
+        .Elements<Edge>(dba->transaction_id())
+        .emplace(global_address.gid(), std::move(old_record),
+                 std::move(new_record));
+    return EdgeAccessor(global_address, *dba);
+  };
+  switch (reader.which()) {
+    case distributed::capnp::TypedValue::VERTEX:
+      *value = load_vertex(reader.getVertex());
+      break;
+    case distributed::capnp::TypedValue::EDGE:
+      *value = load_edge(reader.getEdge());
+      break;
+    case distributed::capnp::TypedValue::PATH: {
+      auto vertices_reader = reader.getPath().getVertices();
+      auto edges_reader = reader.getPath().getEdges();
+      query::Path path(load_vertex(vertices_reader[0]));
+      for (size_t i = 0; i < edges_reader.size(); ++i) {
+        path.Expand(load_edge(edges_reader[i]));
+        path.Expand(load_vertex(vertices_reader[i + 1]));
+      }
+      *value = path;
+      break;
+    }
+    default:
+      LOG(FATAL) << "Unsupported graph element type.";
+  }
+}
+
+ cpp<#)
+
+(lcp:define-rpc pull
+    (:request
+     ((tx-id "tx::TransactionId")
+      (tx-snapshot "tx::Snapshot")
+      (plan-id :int64_t)
+      (command-id "tx::CommandId")
+      (params "Parameters"
+              :save-fun
+              "
+              ar << params.size();
+              for (auto &kv : params) {
+                ar << kv.first;
+                // Params never contain a vertex/edge, so save plan TypedValue.
+                utils::SaveTypedValue(ar, kv.second);
+              }
+              "
+              :load-fun
+              "
+              size_t params_size;
+              ar >> params_size;
+              for (size_t i = 0; i < params_size; ++i) {
+                int token_pos;
+                ar >> token_pos;
+                query::TypedValue param;
+                // Params never contain a vertex/edge, so load plan TypedValue.
+                utils::LoadTypedValue(ar, param);
+                params.Add(token_pos, param);
+              }
+              "
+              :capnp-type "Utils.Map(Utils.BoxInt64, Dis.TypedValue)"
+              :capnp-save
+              (lambda (builder member)
+                #>cpp
+                auto entries_builder = ${builder}.initEntries(${member}.size());
+                size_t i = 0;
+                for (auto &entry : params) {
+                  auto builder = entries_builder[i];
+                  auto key_builder = builder.initKey();
+                  key_builder.setValue(entry.first);
+                  auto value_builder = builder.initValue();
+                  utils::SaveCapnpTypedValue(entry.second, &value_builder);
+                  ++i;
+                }
+                cpp<#)
+              :capnp-load
+              (lambda (reader member)
+                #>cpp
+                for (const auto &entry_reader : ${reader}.getEntries()) {
+                  query::TypedValue value;
+                  utils::LoadCapnpTypedValue(entry_reader.getValue(), &value);
+                  ${member}.Add(entry_reader.getKey().getValue(), value);
+                }
+                cpp<#))
+      (symbols "std::vector<query::Symbol>"
+               :capnp-type "List(Sem.Symbol)"
+               :capnp-save (lcp:capnp-save-vector "query::capnp::Symbol" "query::Symbol")
+               :capnp-load (lcp:capnp-load-vector "query::capnp::Symbol" "query::Symbol"))
+      (accumulate :bool)
+      (batch-size :int64_t)
+      ;; Indicates which of (old, new) records of a graph element should be sent.
+      (send-old :bool)
+      (send-new :bool)))
+  (:response
+   ((data "PullResData" :initarg :move
+          :save-fun
+          "
+          ar << data.pull_state;
+          ar << data.frames.size();
+          // We need to indicate how many values are in each frame.
+          // Assume all the frames have an equal number of elements.
+          ar << (data.frames.size() == 0 ? 0 : data.frames[0].size());
+          for (const auto &frame : data.frames) {
+            for (const auto &value : frame) {
+              utils::SaveTypedValue<TArchive>(
+                  ar, value, [this](TArchive &ar, const query::TypedValue &value) {
+                    data.SaveGraphElement(ar, value);
+                  });
+            }
+          }
+          "
+          :load-fun
+          "
+          ar >> data.pull_state;
+          size_t frame_count;
+          ar >> frame_count;
+          data.frames.reserve(frame_count);
+          size_t frame_size;
+          ar >> frame_size;
+          for (size_t i = 0; i < frame_count; ++i) {
+            data.frames.emplace_back();
+            auto &current_frame = data.frames.back();
+            current_frame.reserve(frame_size);
+            for (size_t j = 0; j < frame_size; ++j) {
+              current_frame.emplace_back();
+              utils::LoadTypedValue<TArchive>(
+                  ar, current_frame.back(),
+                  [this](TArchive &ar, query::TypedValue::TypedValue::Type type,
+                         query::TypedValue &value) {
+                    data.LoadGraphElement(ar, type, value);
+                  });
+            }
+          }
+          "))
+   (:serialize :capnp :base t :load-args '((dba "database::GraphDbAccessor *")))))
+
+;; TODO make a separate RPC for the continuation of an existing pull, as an
+;; optimization not to have to send the full PullReqData pack every time.
+
+(lcp:define-rpc transaction-command-advanced
+    (:request ((member "tx::TransactionId")))
+  (:response ()))
+
+(lcp:pop-namespace) ;; distributed
diff --git a/src/distributed/pull_rpc_clients.cpp b/src/distributed/pull_rpc_clients.cpp
index fc2903546..8652a3830 100644
--- a/src/distributed/pull_rpc_clients.cpp
+++ b/src/distributed/pull_rpc_clients.cpp
@@ -12,53 +12,21 @@ utils::Future<PullData> PullRpcClients::Pull(
     tx::CommandId command_id, const Parameters &params,
     const std::vector<query::Symbol> &symbols, bool accumulate,
     int batch_size) {
-  return clients_.ExecuteOnWorker<PullData>(
-      worker_id, [&dba, plan_id, command_id, params, symbols, accumulate,
-                  batch_size](int worker_id, ClientPool &client_pool) {
-        auto result = client_pool.Call<PullRpc>(
-            dba.transaction_id(), dba.transaction().snapshot(), plan_id,
-            command_id, params, symbols, accumulate, batch_size, true, true);
-
-        auto handle_vertex = [&dba](auto &v) {
-          dba.db()
-              .data_manager()
-              .Elements<Vertex>(dba.transaction_id())
-              .emplace(v.global_address.gid(), std::move(v.old_record),
-                       std::move(v.new_record));
-          if (v.element_in_frame) {
-            VertexAccessor va(v.global_address, dba);
-            *v.element_in_frame = va;
-          }
-        };
-        auto handle_edge = [&dba](auto &e) {
-          dba.db()
-              .data_manager()
-              .Elements<Edge>(dba.transaction_id())
-              .emplace(e.global_address.gid(), std::move(e.old_record),
-                       std::move(e.new_record));
-          if (e.element_in_frame) {
-            EdgeAccessor ea(e.global_address, dba);
-            *e.element_in_frame = ea;
-          }
-        };
-        for (auto &v : result->data.vertices) handle_vertex(v);
-        for (auto &e : result->data.edges) handle_edge(e);
-        for (auto &p : result->data.paths) {
-          handle_vertex(p.vertices[0]);
-          p.path_in_frame =
-              query::Path(VertexAccessor(p.vertices[0].global_address, dba));
-          query::Path &path_in_frame = p.path_in_frame.ValuePath();
-          for (size_t i = 0; i < p.edges.size(); ++i) {
-            handle_edge(p.edges[i]);
-            path_in_frame.Expand(EdgeAccessor(p.edges[i].global_address, dba));
-            handle_vertex(p.vertices[i + 1]);
-            path_in_frame.Expand(
-                VertexAccessor(p.vertices[i + 1].global_address, dba));
-          }
-        }
-
-        return std::move(result->data.state_and_frames);
-      });
+  return clients_.ExecuteOnWorker<
+      PullData>(worker_id, [&dba, plan_id, command_id, params, symbols,
+                            accumulate, batch_size](int worker_id,
+                                                    ClientPool &client_pool) {
+    auto load_pull_res = [&dba](const auto &res_reader) {
+      PullRes res;
+      res.Load(res_reader, &dba);
+      return res;
+    };
+    auto result = client_pool.CallWithLoad<PullRpc>(
+        load_pull_res, dba.transaction_id(), dba.transaction().snapshot(),
+        plan_id, command_id, params, symbols, accumulate, batch_size, true,
+        true);
+    return PullData{result->data.pull_state, std::move(result->data.frames)};
+  });
 }
 
 std::vector<utils::Future<void>>
diff --git a/src/distributed/rpc_worker_clients.hpp b/src/distributed/rpc_worker_clients.hpp
index 5b7c5b043..9fd4cc55a 100644
--- a/src/distributed/rpc_worker_clients.hpp
+++ b/src/distributed/rpc_worker_clients.hpp
@@ -91,9 +91,8 @@ class IndexRpcClients {
         worker_id,
         [label, property, transaction_id](
             int worker_id, communication::rpc::ClientPool &client_pool) {
-          return client_pool.Call<BuildIndexRpc>(
-                     distributed::IndexLabelPropertyTx{
-                         label, property, transaction_id}) != nullptr;
+          return static_cast<bool>(
+              client_pool.Call<BuildIndexRpc>(label, property, transaction_id));
         });
   }
 
diff --git a/src/distributed/serialization.capnp b/src/distributed/serialization.capnp
new file mode 100644
index 000000000..4f51247c1
--- /dev/null
+++ b/src/distributed/serialization.capnp
@@ -0,0 +1,71 @@
+@0xccb448f0b998d9c8;
+
+using Cxx = import "/capnp/c++.capnp";
+$Cxx.namespace("distributed::capnp");
+
+struct Address {
+  gid @0 :UInt64;
+  workerId @1 :Int16;
+}
+
+struct PropertyValue {
+  id @0 :UInt16;
+  value @1 :TypedValue;
+}
+
+struct Edge {
+  from @0 :Address;
+  to @1 :Address;
+  typeId @2 :UInt16;
+  properties @3 :List(PropertyValue);
+}
+
+struct Vertex {
+  outEdges @0 :List(EdgeEntry);
+  inEdges @1 :List(EdgeEntry);
+  labelIds @2 :List(UInt16);
+  properties @3 :List(PropertyValue);
+
+  struct EdgeEntry {
+    vertexAddress @0 :Address;
+    edgeAddress @1 :Address;
+    edgeTypeId @2 :UInt16;
+  }
+}
+
+struct TypedValue {
+  union {
+    nullType @0 :Void;
+    bool @1 :Bool;
+    integer @2 :Int64;
+    double @3 :Float64;
+    string @4 :Text;
+    list @5 :List(TypedValue);
+    map @6 :List(Entry);
+    vertex @7 :VertexAccessor;
+    edge @8 :EdgeAccessor;
+    path @9 :Path;
+  }
+
+  struct Entry {
+    key @0 :Text;
+    value @1 :TypedValue;
+  }
+
+  struct VertexAccessor {
+    address @0 :UInt64;
+    old @1 :Vertex;
+    new @2: Vertex;
+  }
+
+  struct EdgeAccessor {
+    address @0 :UInt64;
+    old @1 :Edge;
+    new @2: Edge;
+  }
+
+  struct Path {
+    vertices @0 :List(VertexAccessor);
+    edges @1 :List(EdgeAccessor);
+  }
+}
diff --git a/src/distributed/serialization.cpp b/src/distributed/serialization.cpp
new file mode 100644
index 000000000..e8c74f831
--- /dev/null
+++ b/src/distributed/serialization.cpp
@@ -0,0 +1,120 @@
+#include "distributed/serialization.hpp"
+
+namespace {
+
+template <class TAddress>
+void SaveAddress(TAddress address,
+                 distributed::capnp::Address::Builder *builder,
+                 int16_t worker_id) {
+  builder->setGid(address.is_local() ? address.local()->gid_ : address.gid());
+  builder->setWorkerId(address.is_local() ? worker_id : address.worker_id());
+}
+
+storage::VertexAddress LoadVertexAddress(
+    const distributed::capnp::Address::Reader &reader) {
+  return {reader.getGid(), reader.getWorkerId()};
+}
+
+storage::EdgeAddress LoadEdgeAddress(
+    const distributed::capnp::Address::Reader &reader) {
+  return {reader.getGid(), reader.getWorkerId()};
+}
+
+void SaveProperties(
+    const PropertyValueStore &props,
+    ::capnp::List<distributed::capnp::PropertyValue>::Builder *builder) {
+  int64_t i = 0;
+  for (const auto &kv : props) {
+    auto prop_builder = (*builder)[i];
+    prop_builder.setId(kv.first.Id());
+    auto value_builder = prop_builder.initValue();
+    utils::SaveCapnpTypedValue(kv.second, &value_builder);
+    ++i;
+  }
+}
+
+PropertyValueStore LoadProperties(
+    const ::capnp::List<distributed::capnp::PropertyValue>::Reader &reader) {
+  PropertyValueStore props;
+  for (const auto &prop_reader : reader) {
+    query::TypedValue value;
+    utils::LoadCapnpTypedValue(prop_reader.getValue(), &value);
+    props.set(storage::Property(prop_reader.getId()), value);
+  }
+  return props;
+}
+
+}  // namespace
+
+namespace distributed {
+
+void SaveVertex(const Vertex &vertex, capnp::Vertex::Builder *builder,
+                int16_t worker_id) {
+  auto save_edges = [worker_id](const auto &edges, auto *edges_builder) {
+    int64_t i = 0;
+    for (const auto &edge : edges) {
+      auto edge_builder = (*edges_builder)[i];
+      auto vertex_addr_builder = edge_builder.initVertexAddress();
+      SaveAddress(edge.vertex, &vertex_addr_builder, worker_id);
+      auto edge_addr_builder = edge_builder.initEdgeAddress();
+      SaveAddress(edge.edge, &edge_addr_builder, worker_id);
+      edge_builder.setEdgeTypeId(edge.edge_type.Id());
+      ++i;
+    }
+  };
+  auto out_builder = builder->initOutEdges(vertex.out_.size());
+  save_edges(vertex.out_, &out_builder);
+  auto in_builder = builder->initInEdges(vertex.in_.size());
+  save_edges(vertex.in_, &in_builder);
+  auto labels_builder = builder->initLabelIds(vertex.labels_.size());
+  for (size_t i = 0; i < vertex.labels_.size(); ++i) {
+    labels_builder.set(i, vertex.labels_[i].Id());
+  }
+  auto properties_builder = builder->initProperties(vertex.properties_.size());
+  SaveProperties(vertex.properties_, &properties_builder);
+}
+
+template <>
+std::unique_ptr<Vertex> LoadVertex(const capnp::Vertex::Reader &reader) {
+  auto vertex = std::make_unique<Vertex>();
+  auto load_edges = [](const auto &edges_reader) {
+    Edges edges;
+    for (const auto &edge_reader : edges_reader) {
+      auto vertex_address = LoadVertexAddress(edge_reader.getVertexAddress());
+      auto edge_address = LoadEdgeAddress(edge_reader.getEdgeAddress());
+      storage::EdgeType edge_type(edge_reader.getEdgeTypeId());
+      edges.emplace(vertex_address, edge_address, edge_type);
+    }
+    return edges;
+  };
+  vertex->out_ = load_edges(reader.getOutEdges());
+  vertex->in_ = load_edges(reader.getInEdges());
+  for (const auto &label_id : reader.getLabelIds()) {
+    vertex->labels_.emplace_back(label_id);
+  }
+  vertex->properties_ = LoadProperties(reader.getProperties());
+  return vertex;
+}
+
+void SaveEdge(const Edge &edge, capnp::Edge::Builder *builder,
+              int16_t worker_id) {
+  auto from_builder = builder->initFrom();
+  SaveAddress(edge.from_, &from_builder, worker_id);
+  auto to_builder = builder->initTo();
+  SaveAddress(edge.to_, &to_builder, worker_id);
+  builder->setTypeId(edge.edge_type_.Id());
+  auto properties_builder = builder->initProperties(edge.properties_.size());
+  SaveProperties(edge.properties_, &properties_builder);
+}
+
+template <>
+std::unique_ptr<Edge> LoadEdge(const capnp::Edge::Reader &reader) {
+  auto from = LoadVertexAddress(reader.getFrom());
+  auto to = LoadVertexAddress(reader.getTo());
+  auto edge =
+      std::make_unique<Edge>(from, to, storage::EdgeType{reader.getTypeId()});
+  edge->properties_ = LoadProperties(reader.getProperties());
+  return edge;
+}
+
+}  // namespace distributed
diff --git a/src/distributed/serialization.hpp b/src/distributed/serialization.hpp
index 468b9f55b..463c3cea5 100644
--- a/src/distributed/serialization.hpp
+++ b/src/distributed/serialization.hpp
@@ -4,6 +4,7 @@
 #include <memory>
 #include <vector>
 
+#include "distributed/serialization.capnp.h"
 #include "storage/address_types.hpp"
 #include "storage/edge.hpp"
 #include "storage/types.hpp"
@@ -38,6 +39,9 @@ void SaveProperties(TArchive &ar, const PropertyValueStore &props) {
 }
 }  // namespace impl
 
+void SaveVertex(const Vertex &vertex, capnp::Vertex::Builder *builder,
+                int16_t worker_id);
+
 /**
  * Saves the given vertex into the given Boost archive.
  *
@@ -68,6 +72,9 @@ void SaveVertex(TArchive &ar, const Vertex &vertex, int worker_id) {
   impl::SaveProperties(ar, vertex.properties_);
 }
 
+void SaveEdge(const Edge &edge, capnp::Edge::Builder *builder,
+              int16_t worker_id);
+
 /**
  * Saves the given edge into the given Boost archive.
  *
@@ -85,6 +92,18 @@ void SaveEdge(TArchive &ar, const Edge &edge, int worker_id) {
   impl::SaveProperties(ar, edge.properties_);
 }
 
+/// Alias for `SaveEdge` allowing for param type resolution.
+inline void SaveElement(const Edge &record, capnp::Edge::Builder *builder,
+                 int16_t worker_id) {
+  return SaveEdge(record, builder, worker_id);
+}
+
+/// Alias for `SaveVertex` allowing for param type resolution.
+inline void SaveElement(const Vertex &record, capnp::Vertex::Builder *builder,
+                 int16_t worker_id) {
+  return SaveVertex(record, builder, worker_id);
+}
+
 /// Alias for `SaveEdge` allowing for param type resolution.
 template <typename TArchive>
 void SaveElement(TArchive &ar, const Edge &record, int worker_id) {
@@ -163,6 +182,9 @@ std::unique_ptr<Vertex> LoadVertex(TArchive &ar) {
   return vertex;
 }
 
+template <>
+std::unique_ptr<Vertex> LoadVertex(const capnp::Vertex::Reader &reader);
+
 /**
  * Loads an Edge from the given archive and returns it.
  *
@@ -181,4 +203,7 @@ std::unique_ptr<Edge> LoadEdge(TArchive &ar) {
   return edge;
 }
 
+template <>
+std::unique_ptr<Edge> LoadEdge(const capnp::Edge::Reader &reader);
+
 }  // namespace distributed
diff --git a/src/distributed/storage_gc_rpc_messages.hpp b/src/distributed/storage_gc_rpc_messages.hpp
deleted file mode 100644
index 716993ede..000000000
--- a/src/distributed/storage_gc_rpc_messages.hpp
+++ /dev/null
@@ -1,39 +0,0 @@
-#pragma once
-
-#include "boost/serialization/access.hpp"
-#include "boost/serialization/base_object.hpp"
-
-#include "communication/rpc/messages.hpp"
-#include "io/network/endpoint.hpp"
-#include "transactions/transaction.hpp"
-
-namespace distributed {
-
-using communication::rpc::Message;
-using Endpoint = io::network::Endpoint;
-
-struct GcClearedStatusReq : public Message {
-  GcClearedStatusReq() {}
-  GcClearedStatusReq(tx::TransactionId local_oldest_active, int worker_id)
-      : local_oldest_active(local_oldest_active), worker_id(worker_id) {}
-
-  tx::TransactionId local_oldest_active;
-  int worker_id;
-
- private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<Message>(*this);
-    ar &local_oldest_active;
-    ar &worker_id;
-  }
-};
-
-RPC_NO_MEMBER_MESSAGE(GcClearedStatusRes);
-
-using RanLocalGcRpc =
-    communication::rpc::RequestResponse<GcClearedStatusReq, GcClearedStatusRes>;
-
-}  // namespace distributed
diff --git a/src/distributed/storage_gc_rpc_messages.lcp b/src/distributed/storage_gc_rpc_messages.lcp
new file mode 100644
index 000000000..a6bff3311
--- /dev/null
+++ b/src/distributed/storage_gc_rpc_messages.lcp
@@ -0,0 +1,20 @@
+#>cpp
+#pragma once
+
+#include "communication/rpc/messages.hpp"
+#include "distributed/storage_gc_rpc_messages.capnp.h"
+#include "io/network/endpoint.hpp"
+#include "transactions/transaction.hpp"
+cpp<#
+
+(lcp:namespace distributed)
+
+(lcp:capnp-namespace "distributed")
+
+(lcp:define-rpc ran-local-gc
+    (:request
+     ((local-oldest-active "tx::TransactionId" :capnp-type "UInt64")
+      (worker-id :int16_t)))
+  (:response ()))
+
+(lcp:pop-namespace) ;; distributed
diff --git a/src/distributed/token_sharing_rpc_messages.hpp b/src/distributed/token_sharing_rpc_messages.hpp
deleted file mode 100644
index a5ed70636..000000000
--- a/src/distributed/token_sharing_rpc_messages.hpp
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma once
-
-#include <memory>
-#include <string>
-
-#include "communication/rpc/messages.hpp"
-#include "distributed/serialization.hpp"
-
-namespace distributed {
-
-RPC_NO_MEMBER_MESSAGE(TokenTransferReq);
-RPC_NO_MEMBER_MESSAGE(TokenTransferRes);
-
-using TokenTransferRpc =
-    communication::rpc::RequestResponse<TokenTransferReq, TokenTransferRes>;
-}  // namespace distributed
diff --git a/src/distributed/token_sharing_rpc_messages.lcp b/src/distributed/token_sharing_rpc_messages.lcp
new file mode 100644
index 000000000..6c3450d05
--- /dev/null
+++ b/src/distributed/token_sharing_rpc_messages.lcp
@@ -0,0 +1,20 @@
+#>cpp
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include "communication/rpc/messages.hpp"
+#include "distributed/serialization.hpp"
+#include "distributed/token_sharing_rpc_messages.capnp.h"
+cpp<#
+
+(lcp:namespace distributed)
+
+(lcp:capnp-namespace "distributed")
+
+(lcp:define-rpc token-transfer
+    (:request ())
+  (:response ()))
+
+(lcp:pop-namespace) ;; distributed
diff --git a/src/distributed/token_sharing_rpc_server.hpp b/src/distributed/token_sharing_rpc_server.hpp
index ead29d56e..a3d8c3fe0 100644
--- a/src/distributed/token_sharing_rpc_server.hpp
+++ b/src/distributed/token_sharing_rpc_server.hpp
@@ -29,10 +29,7 @@ class TokenSharingRpcServer {
         clients_(clients),
         dgp_(db) {
     server_->Register<distributed::TokenTransferRpc>(
-        [this](const distributed::TokenTransferReq &req) {
-          token_ = true;
-          return std::make_unique<distributed::TokenTransferRes>();
-        });
+        [this](const auto &req_reader, auto *res_builder) { token_ = true; });
 
     runner_ = std::thread([this]() {
       while (true) {
diff --git a/src/distributed/transactional_cache_cleaner.hpp b/src/distributed/transactional_cache_cleaner.hpp
index 4644023a1..98e6007fd 100644
--- a/src/distributed/transactional_cache_cleaner.hpp
+++ b/src/distributed/transactional_cache_cleaner.hpp
@@ -72,11 +72,10 @@ class WorkerTransactionalCacheCleaner : public TransactionalCacheCleaner {
         rpc_server_(server),
         produce_server_(produce_server) {
     Register(tx_engine);
-    rpc_server_.Register<WaitOnTransactionEndRpc>(
-        [this](const WaitOnTransactionEndReq &req) {
-          produce_server_.FinishAndClearOngoingProducePlans(req.member);
-          return std::make_unique<WaitOnTransactionEndRes>();
-        });
+    rpc_server_.Register<WaitOnTransactionEndRpc>([this](const auto &req_reader,
+                                                         auto *res_builder) {
+      produce_server_.FinishAndClearOngoingProducePlans(req_reader.getMember());
+    });
   }
 
  private:
diff --git a/src/distributed/transactional_cache_cleaner_rpc_messages.hpp b/src/distributed/transactional_cache_cleaner_rpc_messages.hpp
deleted file mode 100644
index a949ae828..000000000
--- a/src/distributed/transactional_cache_cleaner_rpc_messages.hpp
+++ /dev/null
@@ -1,13 +0,0 @@
-#pragma once
-
-#include "communication/rpc/messages.hpp"
-#include "transactions/type.hpp"
-
-namespace distributed {
-
-RPC_SINGLE_MEMBER_MESSAGE(WaitOnTransactionEndReq, tx::TransactionId);
-RPC_NO_MEMBER_MESSAGE(WaitOnTransactionEndRes);
-using WaitOnTransactionEndRpc =
-    communication::rpc::RequestResponse<WaitOnTransactionEndReq,
-                                        WaitOnTransactionEndRes>;
-};
diff --git a/src/distributed/transactional_cache_cleaner_rpc_messages.lcp b/src/distributed/transactional_cache_cleaner_rpc_messages.lcp
new file mode 100644
index 000000000..7580bd8e3
--- /dev/null
+++ b/src/distributed/transactional_cache_cleaner_rpc_messages.lcp
@@ -0,0 +1,17 @@
+#>cpp
+#pragma once
+
+#include "distributed/transactional_cache_cleaner_rpc_messages.capnp.h"
+#include "communication/rpc/messages.hpp"
+#include "transactions/type.hpp"
+cpp<#
+
+(lcp:namespace distributed)
+
+(lcp:capnp-namespace "distributed")
+
+(lcp:define-rpc wait-on-transaction-end
+    (:request ((member "tx::TransactionId" :capnp-type "UInt64")))
+  (:response ()))
+
+(lcp:pop-namespace)
diff --git a/src/distributed/updates_rpc_clients.cpp b/src/distributed/updates_rpc_clients.cpp
index 42e5f8ef7..0f0b61f20 100644
--- a/src/distributed/updates_rpc_clients.cpp
+++ b/src/distributed/updates_rpc_clients.cpp
@@ -50,7 +50,6 @@ storage::EdgeAddress UpdatesRpcClients::CreateEdge(
     tx::TransactionId tx_id, VertexAccessor &from, VertexAccessor &to,
     storage::EdgeType edge_type) {
   CHECK(from.address().is_remote()) << "In CreateEdge `from` must be remote";
-
   int from_worker = from.address().worker_id();
   auto res = worker_clients_.GetClientPool(from_worker)
                  .Call<CreateEdgeRpc>(CreateEdgeReqData{
diff --git a/src/distributed/updates_rpc_messages.hpp b/src/distributed/updates_rpc_messages.hpp
deleted file mode 100644
index 098a13696..000000000
--- a/src/distributed/updates_rpc_messages.hpp
+++ /dev/null
@@ -1,203 +0,0 @@
-#pragma once
-
-#include <unordered_map>
-
-#include "boost/serialization/vector.hpp"
-
-#include "communication/rpc/messages.hpp"
-#include "database/state_delta.hpp"
-#include "storage/address_types.hpp"
-#include "storage/gid.hpp"
-#include "transactions/type.hpp"
-#include "utils/serialization.hpp"
-
-namespace distributed {
-
-/// The result of sending or applying a deferred update to a worker.
-enum class UpdateResult {
-  DONE,
-  SERIALIZATION_ERROR,
-  LOCK_TIMEOUT_ERROR,
-  UPDATE_DELETED_ERROR,
-  UNABLE_TO_DELETE_VERTEX_ERROR
-};
-
-RPC_SINGLE_MEMBER_MESSAGE(UpdateReq, database::StateDelta);
-RPC_SINGLE_MEMBER_MESSAGE(UpdateRes, UpdateResult);
-using UpdateRpc = communication::rpc::RequestResponse<UpdateReq, UpdateRes>;
-
-RPC_SINGLE_MEMBER_MESSAGE(UpdateApplyReq, tx::TransactionId);
-RPC_SINGLE_MEMBER_MESSAGE(UpdateApplyRes, UpdateResult);
-using UpdateApplyRpc =
-    communication::rpc::RequestResponse<UpdateApplyReq, UpdateApplyRes>;
-
-struct CreateResult {
-  UpdateResult result;
-  // Only valid if creation was successful.
-  gid::Gid gid;
-
- private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &result;
-    ar &gid;
-  }
-};
-
-struct CreateVertexReqData {
-  tx::TransactionId tx_id;
-  std::vector<storage::Label> labels;
-  std::unordered_map<storage::Property, query::TypedValue> properties;
-
- private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void save(TArchive &ar, unsigned int) const {
-    ar << tx_id;
-    ar << labels;
-    ar << properties.size();
-    for (auto &kv : properties) {
-      ar << kv.first;
-      utils::SaveTypedValue(ar, kv.second);
-    }
-  }
-
-  template <class TArchive>
-  void load(TArchive &ar, unsigned int) {
-    ar >> tx_id;
-    ar >> labels;
-    size_t props_size;
-    ar >> props_size;
-    for (size_t i = 0; i < props_size; ++i) {
-      storage::Property p;
-      ar >> p;
-      query::TypedValue tv;
-      utils::LoadTypedValue(ar, tv);
-      properties.emplace(p, std::move(tv));
-    }
-  }
-  BOOST_SERIALIZATION_SPLIT_MEMBER()
-};
-
-RPC_SINGLE_MEMBER_MESSAGE(CreateVertexReq, CreateVertexReqData);
-RPC_SINGLE_MEMBER_MESSAGE(CreateVertexRes, CreateResult);
-using CreateVertexRpc =
-    communication::rpc::RequestResponse<CreateVertexReq, CreateVertexRes>;
-
-struct CreateEdgeReqData {
-  gid::Gid from;
-  storage::VertexAddress to;
-  storage::EdgeType edge_type;
-  tx::TransactionId tx_id;
-
- private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &from;
-    ar &to;
-    ar &edge_type;
-    ar &tx_id;
-  }
-};
-
-RPC_SINGLE_MEMBER_MESSAGE(CreateEdgeReq, CreateEdgeReqData);
-RPC_SINGLE_MEMBER_MESSAGE(CreateEdgeRes, CreateResult);
-using CreateEdgeRpc =
-    communication::rpc::RequestResponse<CreateEdgeReq, CreateEdgeRes>;
-
-struct AddInEdgeReqData {
-  storage::VertexAddress from;
-  storage::EdgeAddress edge_address;
-  gid::Gid to;
-  storage::EdgeType edge_type;
-  tx::TransactionId tx_id;
-
- private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &from;
-    ar &edge_address;
-    ar &to;
-    ar &edge_type;
-    ar &tx_id;
-  }
-};
-
-RPC_SINGLE_MEMBER_MESSAGE(AddInEdgeReq, AddInEdgeReqData);
-RPC_SINGLE_MEMBER_MESSAGE(AddInEdgeRes, UpdateResult);
-using AddInEdgeRpc =
-    communication::rpc::RequestResponse<AddInEdgeReq, AddInEdgeRes>;
-
-struct RemoveVertexReqData {
-  gid::Gid gid;
-  tx::TransactionId tx_id;
-  bool check_empty;
-
- private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &gid;
-    ar &tx_id;
-    ar &check_empty;
-  }
-};
-
-RPC_SINGLE_MEMBER_MESSAGE(RemoveVertexReq, RemoveVertexReqData);
-RPC_SINGLE_MEMBER_MESSAGE(RemoveVertexRes, UpdateResult);
-using RemoveVertexRpc =
-    communication::rpc::RequestResponse<RemoveVertexReq, RemoveVertexRes>;
-
-struct RemoveEdgeData {
-  tx::TransactionId tx_id;
-  gid::Gid edge_id;
-  gid::Gid vertex_from_id;
-  storage::VertexAddress vertex_to_address;
-
- private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &tx_id;
-    ar &edge_id;
-    ar &vertex_from_id;
-    ar &vertex_to_address;
-  }
-};
-
-RPC_SINGLE_MEMBER_MESSAGE(RemoveEdgeReq, RemoveEdgeData);
-RPC_SINGLE_MEMBER_MESSAGE(RemoveEdgeRes, UpdateResult);
-using RemoveEdgeRpc =
-    communication::rpc::RequestResponse<RemoveEdgeReq, RemoveEdgeRes>;
-
-struct RemoveInEdgeData {
-  tx::TransactionId tx_id;
-  gid::Gid vertex;
-  storage::EdgeAddress edge_address;
-
- private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &tx_id;
-    ar &vertex;
-    ar &edge_address;
-  }
-};
-
-RPC_SINGLE_MEMBER_MESSAGE(RemoveInEdgeReq, RemoveInEdgeData);
-RPC_SINGLE_MEMBER_MESSAGE(RemoveInEdgeRes, UpdateResult);
-using RemoveInEdgeRpc =
-    communication::rpc::RequestResponse<RemoveInEdgeReq, RemoveInEdgeRes>;
-
-}  // namespace distributed
diff --git a/src/distributed/updates_rpc_messages.lcp b/src/distributed/updates_rpc_messages.lcp
new file mode 100644
index 000000000..e9be24b4f
--- /dev/null
+++ b/src/distributed/updates_rpc_messages.lcp
@@ -0,0 +1,187 @@
+#>cpp
+#pragma once
+
+#include <unordered_map>
+
+#include "communication/rpc/messages.hpp"
+#include "database/state_delta.hpp"
+#include "distributed/updates_rpc_messages.capnp.h"
+#include "storage/address_types.hpp"
+#include "storage/gid.hpp"
+#include "transactions/type.hpp"
+#include "utils/serialization.hpp"
+cpp<#
+
+(lcp:namespace distributed)
+
+(lcp:capnp-namespace "distributed")
+
+(lcp:capnp-import 'db "/database/state_delta.capnp")
+(lcp:capnp-import 'dis "/distributed/serialization.capnp")
+(lcp:capnp-import 'storage "/storage/serialization.capnp")
+(lcp:capnp-import 'utils "/utils/serialization.capnp")
+
+(lcp:capnp-type-conversion "tx::TransactionId" "UInt64")
+(lcp:capnp-type-conversion "gid::Gid" "UInt64")
+(lcp:capnp-type-conversion "storage::Label" "Storage.Common")
+(lcp:capnp-type-conversion "storage::EdgeType" "Storage.Common")
+(lcp:capnp-type-conversion "storage::Property" "Storage.Common")
+(lcp:capnp-type-conversion "storage::EdgeAddress" "Storage.Address")
+(lcp:capnp-type-conversion "storage::VertexAddress" "Storage.Address")
+
+(lcp:define-enum update-result
+  (done
+   serialization-error
+   lock-timeout-error
+   update-deleted-error
+   unable-to-delete-vertex-error)
+  (:documentation "The result of sending or applying a deferred update to a worker.")
+  (:serialize))
+
+(lcp:define-rpc update
+    (:request ((member "database::StateDelta" :capnp-type "Db.StateDelta")))
+  (:response ((member "UpdateResult"
+                      :capnp-init nil
+                      :capnp-save (lcp:capnp-save-enum "capnp::UpdateResult" "UpdateResult")
+                      :capnp-load (lcp:capnp-load-enum "capnp::UpdateResult" "UpdateResult")))))
+
+(lcp:define-rpc update-apply
+    (:request ((member "tx::TransactionId")))
+  (:response ((member "UpdateResult"
+                      :capnp-init nil
+                      :capnp-save (lcp:capnp-save-enum "capnp::UpdateResult" "UpdateResult")
+                      :capnp-load (lcp:capnp-load-enum "capnp::UpdateResult" "UpdateResult")))))
+
+(lcp:define-struct create-result ()
+  ((result "UpdateResult"
+           :capnp-init nil
+           :capnp-save (lcp:capnp-save-enum "capnp::UpdateResult" "UpdateResult")
+           :capnp-load (lcp:capnp-load-enum "capnp::UpdateResult" "UpdateResult"))
+   (gid "gid::Gid" :documentation "Only valid if creation was successful."))
+  (:serialize :boost :capnp))
+
+(lcp:define-struct create-vertex-req-data ()
+  ((tx-id "tx::TransactionId")
+   (labels "std::vector<storage::Label>"
+     :capnp-save (lcp:capnp-save-vector "storage::capnp::Common" "storage::Label")
+     :capnp-load (lcp:capnp-load-vector "storage::capnp::Common" "storage::Label"))
+   (properties "std::unordered_map<storage::Property, query::TypedValue>"
+               :save-fun
+               #>cpp
+               ar << properties.size();
+               for (auto &kv : properties) {
+                 ar << kv.first;
+                 utils::SaveTypedValue(ar, kv.second);
+               }
+               cpp<#
+               :load-fun
+               #>cpp
+               size_t props_size;
+               ar >> props_size;
+               for (size_t i = 0; i < props_size; ++i) {
+                 storage::Property p;
+                 ar >> p;
+                 query::TypedValue tv;
+                 utils::LoadTypedValue(ar, tv);
+                 properties.emplace(p, std::move(tv));
+               }
+               cpp<#
+               :capnp-type "Utils.Map(Storage.Common, Dis.TypedValue)"
+               :capnp-save
+               (lambda (builder member)
+                 #>cpp
+                 utils::SaveMap<storage::capnp::Common, capnp::TypedValue>(
+                     ${member}, &${builder},
+                     [](auto *builder, const auto &entry) {
+                       auto key_builder = builder->initKey();
+                       entry.first.Save(&key_builder);
+                       auto value_builder = builder->initValue();
+                       utils::SaveCapnpTypedValue(entry.second, &value_builder);
+                     });
+                 cpp<#)
+               :capnp-load
+               (lambda (reader member)
+                 #>cpp
+                 utils::LoadMap<storage::capnp::Common, capnp::TypedValue>(
+                     &${member}, ${reader},
+                     [](const auto &reader) {
+                       storage::Property prop;
+                       prop.Load(reader.getKey());
+                       query::TypedValue value;
+                       utils::LoadCapnpTypedValue(reader.getValue(), &value);
+                       return std::make_pair(prop, value);
+                     });
+                 cpp<#)))
+  (:serialize :capnp))
+
+(lcp:define-rpc create-vertex
+    (:request ((member "CreateVertexReqData")))
+  (:response ((member "CreateResult"))))
+
+(lcp:define-struct create-edge-req-data ()
+  ((from "gid::Gid")
+   (to "storage::VertexAddress")
+   (edge-type "storage::EdgeType")
+   (tx-id "tx::TransactionId"))
+  (:serialize :capnp))
+
+(lcp:define-rpc create-edge
+    (:request ((member "CreateEdgeReqData")))
+  (:response ((member "CreateResult"))))
+
+(lcp:define-struct add-in-edge-req-data ()
+  ((from "storage::VertexAddress")
+   (edge-address "storage::EdgeAddress")
+   (to "gid::Gid")
+   (edge-type "storage::EdgeType")
+   (tx-id "tx::TransactionId"))
+  (:serialize :capnp))
+
+(lcp:define-rpc add-in-edge
+    (:request ((member "AddInEdgeReqData")))
+  (:response ((member "UpdateResult"
+                      :capnp-init nil
+                      :capnp-save (lcp:capnp-save-enum "capnp::UpdateResult" "UpdateResult")
+                      :capnp-load (lcp:capnp-load-enum "capnp::UpdateResult" "UpdateResult")))))
+
+(lcp:define-struct remove-vertex-req-data ()
+  ((gid "gid::Gid")
+   (tx-id "tx::TransactionId")
+   (check-empty :bool))
+  (:serialize :capnp))
+
+(lcp:define-rpc remove-vertex
+    (:request ((member "RemoveVertexReqData")))
+  (:response ((member "UpdateResult"
+                      :capnp-init nil
+                      :capnp-save (lcp:capnp-save-enum "capnp::UpdateResult" "UpdateResult")
+                      :capnp-load (lcp:capnp-load-enum "capnp::UpdateResult" "UpdateResult")))))
+
+(lcp:define-struct remove-edge-data ()
+  ((tx-id "tx::TransactionId")
+   (edge-id "gid::Gid")
+   (vertex-from-id "gid::Gid")
+   (vertex-to-address "storage::VertexAddress"))
+  (:serialize :capnp))
+
+(lcp:define-rpc remove-edge
+    (:request ((member "RemoveEdgeData")))
+  (:response ((member "UpdateResult"
+                      :capnp-init nil
+                      :capnp-save (lcp:capnp-save-enum "capnp::UpdateResult" "UpdateResult")
+                      :capnp-load (lcp:capnp-load-enum "capnp::UpdateResult" "UpdateResult")))))
+
+(lcp:define-struct remove-in-edge-data ()
+  ((tx-id "tx::TransactionId")
+   (vertex "gid::Gid")
+   (edge-address "storage::EdgeAddress"))
+  (:serialize :capnp))
+
+(lcp:define-rpc remove-in-edge
+    (:request ((member "RemoveInEdgeData")))
+  (:response ((member "UpdateResult"
+                      :capnp-init nil
+                      :capnp-save (lcp:capnp-save-enum "capnp::UpdateResult" "UpdateResult")
+                      :capnp-load (lcp:capnp-load-enum "capnp::UpdateResult" "UpdateResult")))))
+
+(lcp:pop-namespace) ;; distributed
diff --git a/src/distributed/updates_rpc_server.cpp b/src/distributed/updates_rpc_server.cpp
index d3b0d9044..106d2d8f5 100644
--- a/src/distributed/updates_rpc_server.cpp
+++ b/src/distributed/updates_rpc_server.cpp
@@ -175,7 +175,9 @@ UpdateResult UpdatesRpcServer::TransactionUpdates<TRecordAccessor>::Apply() {
 UpdatesRpcServer::UpdatesRpcServer(database::GraphDb &db,
                                    communication::rpc::Server &server)
     : db_(db) {
-  server.Register<UpdateRpc>([this](const UpdateReq &req) {
+  server.Register<UpdateRpc>([this](const auto &req_reader, auto *res_builder) {
+    UpdateReq req;
+    req.Load(req_reader);
     using DeltaType = database::StateDelta::Type;
     auto &delta = req.member;
     switch (delta.type) {
@@ -183,74 +185,106 @@ UpdatesRpcServer::UpdatesRpcServer(database::GraphDb &db,
       case DeltaType::ADD_LABEL:
       case DeltaType::REMOVE_LABEL:
       case database::StateDelta::Type::REMOVE_OUT_EDGE:
-      case database::StateDelta::Type::REMOVE_IN_EDGE:
-        return std::make_unique<UpdateRes>(
+      case database::StateDelta::Type::REMOVE_IN_EDGE: {
+        UpdateRes res(
             GetUpdates(vertex_updates_, delta.transaction_id).Emplace(delta));
-      case DeltaType::SET_PROPERTY_EDGE:
-        return std::make_unique<UpdateRes>(
+        res.Save(res_builder);
+        return;
+      }
+      case DeltaType::SET_PROPERTY_EDGE: {
+        UpdateRes res(
             GetUpdates(edge_updates_, delta.transaction_id).Emplace(delta));
+        res.Save(res_builder);
+        return;
+      }
       default:
         LOG(FATAL) << "Can't perform a remote update with delta type: "
                    << static_cast<int>(req.member.type);
     }
   });
 
-  server.Register<UpdateApplyRpc>([this](const UpdateApplyReq &req) {
-    return std::make_unique<UpdateApplyRes>(Apply(req.member));
-  });
+  server.Register<UpdateApplyRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        UpdateApplyReq req;
+        req.Load(req_reader);
+        UpdateApplyRes res(Apply(req.member));
+        res.Save(res_builder);
+      });
 
-  server.Register<CreateVertexRpc>([this](const CreateVertexReq &req) {
+  server.Register<CreateVertexRpc>([this](const auto &req_reader,
+                                          auto *res_builder) {
+    CreateVertexReq req;
+    req.Load(req_reader);
     gid::Gid gid = GetUpdates(vertex_updates_, req.member.tx_id)
                        .CreateVertex(req.member.labels, req.member.properties);
-    return std::make_unique<CreateVertexRes>(
-        CreateResult{UpdateResult::DONE, gid});
+    CreateVertexRes res(CreateResult{UpdateResult::DONE, gid});
+    res.Save(res_builder);
   });
 
-  server.Register<CreateEdgeRpc>([this](const CreateEdgeReq &req) {
+  server.Register<CreateEdgeRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        CreateEdgeReq req;
+        req.Load(req_reader);
+        auto data = req.member;
+        auto creation_result = CreateEdge(data);
+
+        // If `from` and `to` are both on this worker, we handle it in this
+        // RPC call. Do it only if CreateEdge succeeded.
+        if (creation_result.result == UpdateResult::DONE &&
+            data.to.worker_id() == db_.WorkerId()) {
+          auto to_delta = database::StateDelta::AddInEdge(
+              data.tx_id, data.to.gid(), {data.from, db_.WorkerId()},
+              {creation_result.gid, db_.WorkerId()}, data.edge_type);
+          creation_result.result =
+              GetUpdates(vertex_updates_, data.tx_id).Emplace(to_delta);
+        }
+
+        CreateEdgeRes res(creation_result);
+        res.Save(res_builder);
+      });
+
+  server.Register<AddInEdgeRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        AddInEdgeReq req;
+        req.Load(req_reader);
+        auto to_delta = database::StateDelta::AddInEdge(
+            req.member.tx_id, req.member.to, req.member.from,
+            req.member.edge_address, req.member.edge_type);
+        auto result =
+            GetUpdates(vertex_updates_, req.member.tx_id).Emplace(to_delta);
+        AddInEdgeRes res(result);
+        res.Save(res_builder);
+      });
+
+  server.Register<RemoveVertexRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        RemoveVertexReq req;
+        req.Load(req_reader);
+        auto to_delta = database::StateDelta::RemoveVertex(
+            req.member.tx_id, req.member.gid, req.member.check_empty);
+        auto result =
+            GetUpdates(vertex_updates_, req.member.tx_id).Emplace(to_delta);
+        RemoveVertexRes res(result);
+        res.Save(res_builder);
+      });
+
+  server.Register<RemoveEdgeRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        RemoveEdgeReq req;
+        req.Load(req_reader);
+        RemoveEdgeRes res(RemoveEdge(req.member));
+        res.Save(res_builder);
+      });
+
+  server.Register<RemoveInEdgeRpc>([this](const auto &req_reader,
+                                          auto *res_builder) {
+    RemoveInEdgeReq req;
+    req.Load(req_reader);
     auto data = req.member;
-    auto creation_result = CreateEdge(data);
-
-    // If `from` and `to` are both on this worker, we handle it in this
-    // RPC call. Do it only if CreateEdge succeeded.
-    if (creation_result.result == UpdateResult::DONE &&
-        data.to.worker_id() == db_.WorkerId()) {
-      auto to_delta = database::StateDelta::AddInEdge(
-          data.tx_id, data.to.gid(), {data.from, db_.WorkerId()},
-          {creation_result.gid, db_.WorkerId()}, data.edge_type);
-      creation_result.result =
-          GetUpdates(vertex_updates_, data.tx_id).Emplace(to_delta);
-    }
-
-    return std::make_unique<CreateEdgeRes>(creation_result);
-  });
-
-  server.Register<AddInEdgeRpc>([this](const AddInEdgeReq &req) {
-    auto to_delta = database::StateDelta::AddInEdge(
-        req.member.tx_id, req.member.to, req.member.from,
-        req.member.edge_address, req.member.edge_type);
-    auto result =
-        GetUpdates(vertex_updates_, req.member.tx_id).Emplace(to_delta);
-    return std::make_unique<AddInEdgeRes>(result);
-  });
-
-  server.Register<RemoveVertexRpc>([this](const RemoveVertexReq &req) {
-    auto to_delta = database::StateDelta::RemoveVertex(
-        req.member.tx_id, req.member.gid, req.member.check_empty);
-    auto result =
-        GetUpdates(vertex_updates_, req.member.tx_id).Emplace(to_delta);
-    return std::make_unique<RemoveVertexRes>(result);
-  });
-
-  server.Register<RemoveEdgeRpc>([this](const RemoveEdgeReq &req) {
-    return std::make_unique<RemoveEdgeRes>(RemoveEdge(req.member));
-  });
-
-  server.Register<RemoveInEdgeRpc>([this](const RemoveInEdgeReq &req) {
-    auto data = req.member;
-    return std::make_unique<RemoveInEdgeRes>(
-        GetUpdates(vertex_updates_, data.tx_id)
-            .Emplace(database::StateDelta::RemoveInEdge(data.tx_id, data.vertex,
-                                                        data.edge_address)));
+    RemoveInEdgeRes res(GetUpdates(vertex_updates_, data.tx_id)
+                            .Emplace(database::StateDelta::RemoveInEdge(
+                                data.tx_id, data.vertex, data.edge_address)));
+    res.Save(res_builder);
   });
 }
 
diff --git a/src/durability/recovery.capnp b/src/durability/recovery.capnp
new file mode 100644
index 000000000..243b295c6
--- /dev/null
+++ b/src/durability/recovery.capnp
@@ -0,0 +1,9 @@
+@0xb3d70bc0576218f3;
+
+using Cxx = import "/capnp/c++.capnp";
+$Cxx.namespace("durability::capnp");
+
+struct RecoveryInfo {
+  snapshotTxId @0 :UInt64;
+  maxWalTxId @1 :UInt64;
+}
diff --git a/src/durability/recovery.cpp b/src/durability/recovery.cpp
index 3e0b5e930..b09607dea 100644
--- a/src/durability/recovery.cpp
+++ b/src/durability/recovery.cpp
@@ -1,5 +1,6 @@
 #include "durability/recovery.hpp"
 
+#include <experimental/filesystem>
 #include <limits>
 #include <unordered_map>
 
@@ -16,6 +17,8 @@
 #include "transactions/type.hpp"
 #include "utils/algorithm.hpp"
 
+namespace fs = std::experimental::filesystem;
+
 namespace durability {
 
 bool ReadSnapshotSummary(HashedFileReader &buffer, int64_t &vertex_count,
diff --git a/src/durability/recovery.hpp b/src/durability/recovery.hpp
index ccb8b5f28..87cfe6c11 100644
--- a/src/durability/recovery.hpp
+++ b/src/durability/recovery.hpp
@@ -1,16 +1,14 @@
 #pragma once
 
-#include <experimental/filesystem>
 #include <experimental/optional>
 #include <unordered_map>
 
 #include "database/graph_db.hpp"
 #include "durability/hashed_file_reader.hpp"
+#include "durability/recovery.capnp.h"
 #include "storage/vertex_accessor.hpp"
 #include "transactions/type.hpp"
 
-namespace fs = std::experimental::filesystem;
-
 namespace durability {
 
 /// Stores info on what was (or needs to be) recovered from durability.
@@ -28,6 +26,16 @@ struct RecoveryInfo {
   }
   bool operator!=(const RecoveryInfo &other) const { return !(*this == other); }
 
+  void Save(capnp::RecoveryInfo::Builder *builder) const {
+    builder->setSnapshotTxId(snapshot_tx_id);
+    builder->setMaxWalTxId(max_wal_tx_id);
+  }
+
+  void Load(const capnp::RecoveryInfo::Reader &reader) {
+    snapshot_tx_id = reader.getSnapshotTxId();
+    max_wal_tx_id = reader.getMaxWalTxId();
+  }
+
  private:
   friend class boost::serialization::access;
 
diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt
index 6c96a87ad..10cb3263a 100644
--- a/src/io/CMakeLists.txt
+++ b/src/io/CMakeLists.txt
@@ -4,7 +4,29 @@ set(io_src_files
     network/socket.cpp
     network/utils.cpp)
 
+# Use this function to add each capnp file to generation. This way each file is
+# standalone and we avoid recompiling everything.
+# NOTE: io_src_files and io_capnp_files are globally updated.
+# TODO: This is duplicated from src/CMakeLists.txt, find a good way to
+# generalize this on per subdirectory basis.
+function(add_capnp capnp_src_file)
+  set(cpp_file ${CMAKE_CURRENT_SOURCE_DIR}/${capnp_src_file}.c++)
+  set(h_file ${CMAKE_CURRENT_SOURCE_DIR}/${capnp_src_file}.h)
+  add_custom_command(OUTPUT ${cpp_file} ${h_file}
+    COMMAND ${CAPNP_EXE} compile -o${CAPNP_CXX_EXE} ${capnp_src_file} -I ${CMAKE_CURRENT_SOURCE_DIR}
+    DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${capnp_src_file} capnproto-proj
+    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+  # Update *global* io_capnp_files
+  set(io_capnp_files ${io_capnp_files} ${cpp_file} ${h_file} PARENT_SCOPE)
+  # Update *global* io_src_files
+  set(io_src_files ${io_src_files} ${cpp_file} PARENT_SCOPE)
+endfunction(add_capnp)
+
+add_capnp(network/endpoint.capnp)
+
+add_custom_target(generate_io_capnp DEPENDS ${io_capnp_files})
+
 add_library(mg-io STATIC ${io_src_files})
 target_link_libraries(mg-io stdc++fs Threads::Threads fmt glog mg-utils)
-# TODO: Remove this dependency when we switch to capnp
-target_link_libraries(mg-io ${Boost_SERIALIZATION_LIBRARY_RELEASE})
+target_link_libraries(mg-io capnp kj)
+add_dependencies(mg-io generate_io_capnp)
diff --git a/src/io/network/endpoint.capnp b/src/io/network/endpoint.capnp
new file mode 100644
index 000000000..bc58b2869
--- /dev/null
+++ b/src/io/network/endpoint.capnp
@@ -0,0 +1,10 @@
+@0x93c2449a1e02365a;
+
+using Cxx = import "/capnp/c++.capnp";
+$Cxx.namespace("io::network::capnp");
+
+struct Endpoint {
+  address @0 :Text;
+  port @1 :UInt16;
+  family @2 :UInt8;
+}
diff --git a/src/io/network/endpoint.cpp b/src/io/network/endpoint.cpp
index 9761d1b60..c1f94bb22 100644
--- a/src/io/network/endpoint.cpp
+++ b/src/io/network/endpoint.cpp
@@ -24,6 +24,18 @@ Endpoint::Endpoint(const std::string &address, uint16_t port)
   CHECK(family_ != 0) << "Not a valid IPv4 or IPv6 address: " << address;
 }
 
+void Endpoint::Save(capnp::Endpoint::Builder *builder) const {
+  builder->setAddress(address_);
+  builder->setPort(port_);
+  builder->setFamily(family_);
+}
+
+void Endpoint::Load(const capnp::Endpoint::Reader &reader) {
+  address_ = reader.getAddress();
+  port_ = reader.getPort();
+  family_ = reader.getFamily();
+}
+
 bool Endpoint::operator==(const Endpoint &other) const {
   return address_ == other.address_ && port_ == other.port_ &&
          family_ == other.family_;
diff --git a/src/io/network/endpoint.hpp b/src/io/network/endpoint.hpp
index 5c7e8a477..bc17ccfd3 100644
--- a/src/io/network/endpoint.hpp
+++ b/src/io/network/endpoint.hpp
@@ -5,8 +5,7 @@
 #include <iostream>
 #include <string>
 
-#include "boost/serialization/access.hpp"
-
+#include "io/network/endpoint.capnp.h"
 #include "utils/exceptions.hpp"
 
 namespace io::network {
@@ -28,16 +27,10 @@ class Endpoint {
   bool operator==(const Endpoint &other) const;
   friend std::ostream &operator<<(std::ostream &os, const Endpoint &endpoint);
 
+  void Save(capnp::Endpoint::Builder *builder) const;
+  void Load(const capnp::Endpoint::Reader &reader);
+
  private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &address_;
-    ar &port_;
-    ar &family_;
-  }
-
   std::string address_;
   uint16_t port_{0};
   unsigned char family_{0};
diff --git a/src/lisp/lcp.lisp b/src/lisp/lcp.lisp
index 54f097aca..84736c9fe 100644
--- a/src/lisp/lcp.lisp
+++ b/src/lisp/lcp.lisp
@@ -3,11 +3,20 @@
   (:export #:define-class
            #:define-struct
            #:define-enum
+           #:define-rpc
+           #:cpp-list
+           #:in-impl
            #:namespace
            #:pop-namespace
            #:capnp-namespace
            #:capnp-import
            #:capnp-type-conversion
+           #:capnp-save-optional
+           #:capnp-load-optional
+           #:capnp-save-vector
+           #:capnp-load-vector
+           #:capnp-save-enum
+           #:capnp-load-enum
            #:process-file))
 
 (in-package #:lcp)
@@ -75,21 +84,57 @@
 (eval-when (:compile-toplevel :load-toplevel :execute)
   (set-dispatch-macro-character #\# #\> #'|#>-reader|))
 
-(deftype cpp-primitive-type ()
-  `(member :bool :int :int32_t :int64_t :uint :uint32_t :uint64_t :float :double))
+(defclass cpp-type ()
+  ((documentation :type (or null string) :initarg :documentation :initform nil
+                  :reader cpp-type-documentation
+                  :documentation "Documentation string for this C++ type.")
+   (namespace :type list :initarg :ns :initarg :namespace :initform nil
+              :reader cpp-type-namespace
+              :documentation "A list of symbols or strings defining the full
+              namespace. A single symbol may refer to a `CPP-CLASS' which
+              encloses this type.")
+   (enclosing-class :type (or null symbol) :initarg :enclosing-class
+                    :initform nil :accessor cpp-type-enclosing-class)
+   (name :type (or symbol string) :initarg :name :reader cpp-type-base-name
+         :documentation "Base name of this type.")
+   (type-params :type list :initarg :type-params :initform nil
+                :reader cpp-type-type-params
+                :documentation "List of template parameters that are needed to
+                instantiate a concrete type. For example, in `template
+                <TValue> class vector`, 'TValue' is type parameter.")
+   (type-args :type list :initarg :type-args :initform nil
+              :reader cpp-type-type-args
+              :documentation "List of already applied template arguments. For
+              example in `std::vector<int>`, 'int' is a type argument."))
+  (:documentation "Base class for meta information on C++ types."))
 
-(defun cpp-primitive-type-p (type)
-  (member type '(:bool :int :int32_t :int64_t :uint :uint32_t :uint64_t :float :double)))
+(defgeneric cpp-type-name (cpp-type)
+  (:documentation "Get C++ style type name from `CPP-TYPE' as a string."))
 
-;; TODO: Rename this to cpp-type and set it as the base class/struct for all
-;; the other cpp type metainformation (`CPP-CLASS', `CPP-ENUM' ...)
-(defstruct cpp-type-decl
-  namespace
-  name
-  type-args)
+(defmethod cpp-type-name ((cpp-type string))
+  "Return CPP-TYPE string as is."
+  cpp-type)
+
+(defmethod cpp-type-name ((cpp-type cpp-type))
+  "Return `CPP-TYPE' name as PascalCase or if string, as is."
+  (cpp-type-name (cpp-type-base-name cpp-type)))
+
+(deftype cpp-primitive-type-keywords ()
+  "List of keywords that specify a primitive type in C++."
+  `(member :bool :int :int16_t :int32_t :int64_t :uint :uint32_t :uint64_t :float :double))
+
+(defmethod cpp-type-name ((cpp-type symbol))
+  "Return PascalCase of CPP-TYPE symbol or lowercase if it is a primitive type."
+  (if (typep cpp-type 'cpp-primitive-type-keywords)
+      (string-downcase (string cpp-type))
+      (remove #\- (string-capitalize (string cpp-type)))))
+
+(defclass cpp-primitive-type (cpp-type)
+  ((name :type cpp-primitive-type-keywords))
+  (:documentation "Represents a primitive type in C++."))
 
 (defun parse-cpp-type-declaration (type-decl)
-  "Parse C++ type from TYPE-DECL string and return CPP-TYPE-DECL.
+  "Parse C++ type from TYPE-DECL string and return CPP-TYPE.
 
 For example:
 
@@ -97,14 +142,14 @@ For example:
 
 produces:
 
-;; (cpp-type-decl
+;; (cpp-type
 ;;  :name pair
-;;  :type-args ((cpp-type-decl
+;;  :type-args ((cpp-type
 ;;              :name MyClass
-;;              :type-args ((cpp-type-decl :name function
-;;                                         :type-args (cpp-type-decl :name void(int, bool)))
-;;                          (cpp-type-decl :name double)))
-;;              (cpp-type-decl :name char)))"
+;;              :type-args ((cpp-type :name function
+;;                                    :type-args (cpp-type :name void(int, bool)))
+;;                          (cpp-type :name double)))
+;;              (cpp-type :name char)))"
   (declare (type string type-decl))
   ;; C++ type can be declared as follows:
   ;; namespace::namespace::type<type-arg, type-arg> *
@@ -140,31 +185,33 @@ produces:
                        (subseq template arg-start (1- match-end)))
                       type-args)
                 (setf arg-start (1+ match-end)))))))
-      (make-cpp-type-decl :namespace (when (cdr namespace-split)
-                                       (butlast namespace-split))
-                          :name name
-                          :type-args (reverse type-args)))))
+      (make-instance 'cpp-type
+                     :ns (when (cdr namespace-split)
+                           (butlast namespace-split))
+                     :name name
+                     :type-args (reverse type-args)))))
 
-(defun string<-cpp-type-decl (type-decl)
-  (declare (type cpp-type-decl type-decl))
+(defun cpp-type-decl (cpp-type)
+  (declare (type cpp-type cpp-type))
+  ;; TODO: Merge this and cpp-class-full-name
   (with-output-to-string (s)
-    (format s "~{~A::~}" (cpp-type-decl-namespace type-decl))
-    (write-string (cpp-type-decl-name type-decl) s)
-    (when (cpp-type-decl-type-args type-decl)
-      (format s "<~{~A~^, ~}>"
-              (mapcar #'string<-cpp-type-decl (cpp-type-decl-type-args type-decl))))))
+    (format s "~{~A::~}" (cpp-type-namespace cpp-type))
+    (write-string (cpp-type-name cpp-type) s)
+    (when (cpp-type-type-args cpp-type)
+      (format s "<~{~A~^, ~}>" (mapcar #'cpp-type-decl (cpp-type-type-args cpp-type))))))
 
-(defstruct cpp-enum
-  "Meta information on a C++ enum."
-  (symbol nil :type symbol :read-only t)
-  (documentation nil :type (or null string) :read-only t)
-  (values nil :read-only t)
-  (enclosing-class nil :type (or null symbol) :read-only t))
+(defclass cpp-enum (cpp-type)
+  ((values :type list :initarg :values :initform nil :reader cpp-enum-values)
+   ;; If true, generate the schema for this enum.
+   (capnp-schema :type boolean :initarg :capnp-schema :initform nil
+                 :reader cpp-enum-capnp-schema))
+  (:documentation "Meta information on a C++ enum."))
 
 (defstruct cpp-member
   "Meta information on a C++ class (or struct) member variable."
   (symbol nil :type symbol :read-only t)
-  (type nil :type (or cpp-primitive-type string) :read-only t)
+  (type nil :type (or cpp-primitive-type-keywords string) :read-only t)
+  (initarg nil :type symbol :read-only t)
   (initval nil :type (or null string integer float) :read-only t)
   (scope :private :type (member :public :protected :private) :read-only t)
   ;; TODO: Support giving a name for reader function.
@@ -174,9 +221,11 @@ produces:
   ;; args: (archive member-name) and needs to return C++ code.
   (save-fun nil :type (or null string raw-cpp function) :read-only t)
   (load-fun nil :type (or null string raw-cpp function) :read-only t)
-  (capnp-type nil :type (or null string) :read-only t)
+  ;; CAPNP-TYPE may be a string specifying the type, or a list of
+  ;; (member-symbol "capnp-type") specifying a union type.
+  (capnp-type nil :type (or null string list) :read-only t)
   (capnp-init t :type boolean :read-only t)
-  (capnp-save nil :type (or null function) :read-only t)
+  (capnp-save nil :type (or null function (eql :dont-save)) :read-only t)
   (capnp-load nil :type (or null function) :read-only t))
 
 (defstruct capnp-opts
@@ -193,47 +242,44 @@ produces:
   ;; as a composition.
   (inherit-compose nil :read-only t))
 
-(defstruct cpp-class
-  "Meta information on a C++ class (or struct)."
-  (structp nil :type boolean :read-only t)
-  (name nil :type symbol :read-only t)
-  (super-classes nil :read-only t)
-  (type-params nil :read-only t)
-  (documentation "" :type (or null string) :read-only t)
-  (members nil :read-only t)
-  ;; Custom C++ code in 3 scopes. May be a list of C++ meta information or a
-  ;; single element.
-  (public nil :read-only t)
-  (protected nil :read-only t)
-  (private nil)
-  (capnp-opts nil :type (or null capnp-opts) :read-only t)
-  (namespace nil :read-only t)
-  (inner-types nil)
-  (enclosing-class nil :type (or null symbol)))
-
-(defun cpp-type-name (name)
-  "Get C++ style type name from NAME as a string."
-  (typecase name
-    (cpp-primitive-type (string-downcase (string name)))
-    (symbol (remove #\- (string-capitalize (string name))))
-    (string name)
-    (otherwise (error "Unkown conversion to C++ type for ~S" (type-of name)))))
+(defclass cpp-class (cpp-type)
+  ((structp :type boolean :initarg :structp :initform nil
+            :reader cpp-class-structp)
+   (super-classes :initarg :super-classes :initform nil
+                  :reader cpp-class-super-classes)
+   (members :initarg :members :initform nil :reader cpp-class-members)
+   ;; Custom C++ code in 3 scopes. May be a list of C++ meta information or a
+   ;; single element.
+   (public :initarg :public :initform nil :reader cpp-class-public)
+   (protected :initarg :protected :initform nil :reader cpp-class-protected)
+   (private :initarg :private :initform nil :accessor cpp-class-private)
+   (capnp-opts :type (or null capnp-opts) :initarg :capnp-opts :initform nil
+               :reader cpp-class-capnp-opts)
+   (inner-types :initarg :inner-types :initform nil :reader cpp-class-inner-types))
+  (:documentation "Meta information on a C++ class (or struct)."))
 
 (defvar *cpp-classes* nil "List of defined classes from LCP file")
+(defvar *cpp-enums* nil "List of defined enums from LCP file")
 
 (defun find-cpp-class (cpp-class-name)
   "Find CPP-CLASS in *CPP-CLASSES* by CPP-CLASS-NAME"
   (declare (type (or symbol string) cpp-class-name))
+  ;; TODO: Find by full name
   (if (stringp cpp-class-name)
-      (find cpp-class-name *cpp-classes*
-            :key (lambda (class) (cpp-type-name (cpp-class-name class)))
-            :test #'string=)
-      (find cpp-class-name *cpp-classes* :key #'cpp-class-name)))
+      (find cpp-class-name *cpp-classes* :key #'cpp-type-name :test #'string=)
+      (find cpp-class-name *cpp-classes* :key #'cpp-type-base-name)))
+
+(defun find-cpp-enum (cpp-enum-name)
+  "Find CPP-ENUM in *CPP-ENUMS* by CPP-ENUM-NAME"
+  (declare (type (or symbol string) cpp-enum-name))
+  (if (stringp cpp-enum-name)
+      (find cpp-enum-name *cpp-enums* :key #'cpp-type-name :test #'string=)
+      (find cpp-enum-name *cpp-enums* :key #'cpp-type-base-name)))
 
 (defun direct-subclasses-of (cpp-class)
   "Find direct subclasses of CPP-CLASS from *CPP-CLASSES*"
   (declare (type (or symbol cpp-class) cpp-class))
-  (let ((name (if (symbolp cpp-class) cpp-class (cpp-class-name cpp-class))))
+  (let ((name (if (symbolp cpp-class) cpp-class (cpp-type-base-name cpp-class))))
     (reverse ;; reverse to get them in definition order
      (remove-if (lambda (subclass)
                   (not (member name (cpp-class-super-classes subclass))))
@@ -244,9 +290,7 @@ produces:
   (declare (type string documentation))
   (format nil "/// ~A"
           (cl-ppcre:regex-replace-all
-           (string #\Newline)
-           documentation
-           (format nil "~%/// "))))
+           (string #\Newline) documentation (format nil "~%/// "))))
 
 (defun cpp-variable-name (symbol)
   "Get C++ style name of SYMBOL as a string."
@@ -270,9 +314,9 @@ produces:
   "Get C++ style `CPP-ENUM' definition as a string."
   (declare (type cpp-enum cpp-enum))
   (with-output-to-string (s)
-    (when (cpp-enum-documentation cpp-enum)
-      (write-line (cpp-documentation (cpp-enum-documentation cpp-enum)) s))
-    (format s "enum class ~A {~%" (cpp-type-name (cpp-enum-symbol cpp-enum)))
+    (when (cpp-type-documentation cpp-enum)
+      (write-line (cpp-documentation (cpp-type-documentation cpp-enum)) s))
+    (format s "enum class ~A {~%" (cpp-type-name cpp-enum))
     (format s "~{ ~A~^,~%~}~%" (mapcar #'cpp-constant-name (cpp-enum-values cpp-enum)))
     (write-line "};" s)))
 
@@ -292,7 +336,7 @@ produces:
 (defun cpp-member-reader-definition (cpp-member)
   "Get C++ style `CPP-MEMBER' getter (reader) function."
   (declare (type cpp-member cpp-member))
-  (if (cpp-primitive-type-p (cpp-member-type cpp-member))
+  (if (typep (cpp-member-type cpp-member) 'cpp-primitive-type-keywords)
       (format nil "auto ~A() const { return ~A; }" (cpp-member-name cpp-member :struct t) (cpp-member-name cpp-member))
       (format nil "const auto &~A() const { return ~A; }" (cpp-member-name cpp-member :struct t) (cpp-member-name cpp-member))))
 
@@ -312,20 +356,20 @@ NIL, returns a string."
            (cpp-member-declaration member :struct (cpp-class-structp cpp-class))))
     (with-output-to-string (s)
       (terpri s)
-      (when (cpp-class-documentation cpp-class)
-        (write-line (cpp-documentation (cpp-class-documentation cpp-class)) s))
-      (when (cpp-class-type-params cpp-class)
-        (cpp-template (cpp-class-type-params cpp-class) s))
+      (when (cpp-type-documentation cpp-class)
+        (write-line (cpp-documentation (cpp-type-documentation cpp-class)) s))
+      (when (cpp-type-type-params cpp-class)
+        (cpp-template (cpp-type-type-params cpp-class) s))
       (if (cpp-class-structp cpp-class)
           (write-string "struct " s)
           (write-string "class " s))
-      (format s "~A" (cpp-type-name (cpp-class-name cpp-class)))
+      (format s "~A" (cpp-type-name cpp-class))
       (when (cpp-class-super-classes cpp-class)
         (format s " : ~{public ~A~^, ~}"
                 (mapcar #'cpp-type-name (cpp-class-super-classes cpp-class))))
       (write-line " {" s)
-      (let ((reader-members (remove-if (lambda (m) (not (cpp-member-reader m)))
-                                         (cpp-class-members cpp-class))))
+      (let ((reader-members (remove-if (complement #'cpp-member-reader)
+                                       (cpp-class-members cpp-class))))
         (when (or (cpp-class-public cpp-class) (cpp-class-members-scoped :public) reader-members)
           (unless (cpp-class-structp cpp-class)
             (write-line " public:" s))
@@ -337,9 +381,9 @@ NIL, returns a string."
         (let ((save (capnp-save-declaration cpp-class))
               (construct (capnp-construct-declaration cpp-class))
               (load (capnp-load-declaration cpp-class)))
-          (when save (format s "  ~A;~%" save))
-          (when construct (format s "  ~A;~%" construct))
-          (when load (format s "  ~A;~%" load))))
+          (when save (format s "  ~A;~2%" save))
+          (when construct (format s "  ~A;~2%" construct))
+          (when load (format s "  ~A;~2%" load))))
       (when (or (cpp-class-protected cpp-class) (cpp-class-members-scoped :protected))
         (write-line " protected:" s)
         (format s "~{~A~%~}" (mapcar #'cpp-code (cpp-class-protected cpp-class)))
@@ -360,14 +404,14 @@ NIL, returns a string."
            (let (enclosing)
              (loop
                 for class = cpp-class
-                then (find-cpp-class (cpp-class-enclosing-class class))
+                then (find-cpp-class (cpp-type-enclosing-class class))
                 while class
-                do (push (cpp-type-name (cpp-class-name class)) enclosing))
+                do (push (cpp-type-name class) enclosing))
              enclosing)))
     (let* ((full-name (format nil "~{~A~^::~}" (enclosing-classes class)))
-           (type-args (if (or (not type-args) (not (cpp-class-type-params class)))
+           (type-args (if (or (not type-args) (not (cpp-type-type-params class)))
                           ""
-                          (format nil "<~{~A~^, ~}>" (mapcar #'cpp-type-name (cpp-class-type-params class))))))
+                          (format nil "<~{~A~^, ~}>" (mapcar #'cpp-type-name (cpp-type-type-params class))))))
       (concatenate 'string full-name type-args))))
 
 (defun cpp-method-declaration (class method-name
@@ -380,18 +424,17 @@ declaration to be used outside of class definition.  Remaining keys are flags
 which generate the corresponding C++ keywords."
   (declare (type cpp-class class)
            (type string method-name))
-  (let* ((type-params (cpp-class-type-params class))
+  (let* ((type-params (cpp-type-type-params class))
          (template (if (or inline (not type-params)) "" (cpp-template type-params)))
          (static/virtual (cond
                            ((and inline static) "static")
                            ((and inline virtual) "virtual")
                            (t "")))
          (namespace (if inline "" (format nil "~A::" (cpp-class-full-name class))))
-         (args (format nil "~{~A~^, ~}"
+         (args (format nil "~:{~A ~A~:^, ~}"
                        (mapcar (lambda (name-and-type)
-                                 (format nil "~A ~A"
-                                         (cpp-type-name (second name-and-type))
-                                         (cpp-variable-name (first name-and-type))))
+                                 (list (cpp-type-name (second name-and-type))
+                                       (cpp-variable-name (first name-and-type))))
                                args)))
          (const (if const "const" ""))
          (override (if (and override inline) "override" "")))
@@ -401,6 +444,15 @@ which generate the corresponding C++ keywords."
      ${returns} ${namespace}${method-name}(${args}) ${const} ${override}
      cpp<#)))
 
+(defstruct cpp-list
+  values)
+
+(defun cpp-list (&rest args)
+  (make-cpp-list
+   :values (remove-if (lambda (a)
+                        (not (typep a '(or raw-cpp cpp-type cpp-list))))
+                      args)))
+
 (defun cpp-code (cpp)
   "Get a C++ string from given CPP meta information."
   (typecase cpp
@@ -408,6 +460,7 @@ which generate the corresponding C++ keywords."
     (cpp-class (cpp-class-definition cpp))
     (cpp-enum (cpp-enum-definition cpp))
     (string cpp)
+    (cpp-list (format nil "~{~A~^~%~}" (mapcar #'cpp-code (cpp-list-values cpp))))
     (null "")
     (otherwise (error "Unknown conversion to C++ for ~S" (type-of cpp)))))
 
@@ -530,7 +583,7 @@ which generate the corresponding C++ keywords."
   "Get direct subclasses of CPP-CLASS which should be modeled as a union in
 Cap'n Proto schema."
   (declare (type (or symbol cpp-class) cpp-class))
-  (let ((class-name (if (symbolp cpp-class) cpp-class (cpp-class-name cpp-class))))
+  (let ((class-name (if (symbolp cpp-class) cpp-class (cpp-type-base-name cpp-class))))
     (remove-if (lambda (subclass)
                  (member class-name
                          (capnp-opts-inherit-compose (cpp-class-capnp-opts subclass))))
@@ -544,10 +597,11 @@ CPP-CLASS."
   (let* ((class (if (symbolp cpp-class) (find-cpp-class cpp-class) cpp-class))
          (capnp-opts (cpp-class-capnp-opts class))
          union compose)
-    (dolist (parent (cpp-class-super-classes class))
-      (if (member parent (capnp-opts-inherit-compose capnp-opts))
-          (push parent compose)
-          (push parent union)))
+    (when (not (capnp-opts-base capnp-opts))
+      (dolist (parent (cpp-class-super-classes class))
+        (if (member parent (capnp-opts-inherit-compose capnp-opts))
+            (push parent compose)
+            (push parent union))))
     (values union compose)))
 
 (defun capnp-union-parents-rec (cpp-class)
@@ -560,7 +614,7 @@ encoded as union inheritance in Cap'n Proto."
                (and opts (capnp-opts-base opts))))
            (rec (class)
              (declare (type cpp-class class))
-             (cons (cpp-class-name class)
+             (cons (cpp-type-base-name class)
                    ;; Continue to supers only if this isn't marked as capnp base class.
                    (when (and (not (capnp-base-p class))
                               (capnp-union-and-compose-parents class))
@@ -580,45 +634,72 @@ encoded as union inheritance in Cap'n Proto."
   (push (cons cpp-type capnp-type) *capnp-type-converters*))
 
 (defun capnp-type<-cpp-type (cpp-type)
-  (typecase cpp-type
-    (cpp-primitive-type
-     (when (member cpp-type '(:int :uint))
-       (error "Unable to get Capnp type for integer without specified width."))
-     ;; Delete the _t suffix
-     (cl-ppcre:regex-replace "_t$" (string-downcase cpp-type :start 1) ""))
-    (string
-     (let ((type-decl (parse-cpp-type-declaration cpp-type)))
-       (cond
-         ((string= "shared_ptr" (cpp-type-decl-name type-decl))
-          (let ((class (find-cpp-class
-                        ;; TODO: Use full type
-                        (cpp-type-decl-name (first (cpp-type-decl-type-args type-decl))))))
-            (unless class
-              (error "Unable to determine base type for '~A'; use :capnp-type"
-                     cpp-type))
-            (let* ((parents (capnp-union-parents-rec class))
-                   (top-parent (if parents (car (last parents)) (cpp-class-name class))))
-                   (format nil "Utils.SharedPtr(~A)" (cpp-type-name top-parent)))))
-         ((string= "vector" (cpp-type-decl-name type-decl))
-          (format nil "List(~A)"
-                  (capnp-type<-cpp-type
-                   (string<-cpp-type-decl (first (cpp-type-decl-type-args type-decl))))))
-         ((string= "optional" (cpp-type-decl-name type-decl))
-          (format nil "Utils.Optional(~A)"
-                  (capnp-type<-cpp-type
-                   (string<-cpp-type-decl (first (cpp-type-decl-type-args type-decl))))))
-         ((assoc cpp-type *capnp-type-converters* :test #'string=)
-          (cdr (assoc cpp-type *capnp-type-converters* :test #'string=)))
-         (t (cpp-type-name cpp-type)))))
-    ;; Capnp only accepts uppercase first letter in types (PascalCase), so
-    ;; this is the same as our conversion to C++ type name.
-    (otherwise (cpp-type-name cpp-type))))
+  (flet ((convert-primitive-type (name)
+           (when (member name '(:int :uint))
+             (error "Unable to get Capnp type for integer without specified width."))
+           (case name
+             (:bool "Bool")
+             (:float "Float32")
+             (:double "Float64")
+             (otherwise
+              (let ((pos-of-i (position #\I (string name))))
+                ;; Delete the _t suffix
+                (cl-ppcre:regex-replace
+                 "_t$" (string-downcase name :start (1+ pos-of-i)) ""))))))
+    (typecase cpp-type
+      (cpp-primitive-type-keywords (convert-primitive-type cpp-type))
+      (cpp-primitive-type (convert-primitive-type (cpp-type-base-name cpp-type)))
+      (string
+       (let ((type (parse-cpp-type-declaration cpp-type)))
+         (cond
+           ((string= "string" (cpp-type-base-name type))
+            "Text")
+           ((string= "shared_ptr" (cpp-type-base-name type))
+            (let ((class (find-cpp-class
+                          ;; TODO: Use full type
+                          (cpp-type-base-name (first (cpp-type-type-args type))))))
+              (unless class
+                (error "Unable to determine base type for '~A'; use :capnp-type"
+                       cpp-type))
+              (let* ((parents (capnp-union-parents-rec class))
+                     (top-parent (if parents (car (last parents)) (cpp-type-base-name class))))
+                (format nil "Utils.SharedPtr(~A)" (cpp-type-name top-parent)))))
+           ((string= "vector" (cpp-type-base-name type))
+            (format nil "List(~A)"
+                    (capnp-type<-cpp-type
+                     (cpp-type-decl (first (cpp-type-type-args type))))))
+           ((string= "optional" (cpp-type-base-name type))
+            (format nil "Utils.Optional(~A)"
+                    (capnp-type<-cpp-type (cpp-type-decl (first (cpp-type-type-args type))))))
+           ((assoc cpp-type *capnp-type-converters* :test #'string=)
+            (cdr (assoc cpp-type *capnp-type-converters* :test #'string=)))
+           (t (cpp-type-name cpp-type)))))
+      ;; Capnp only accepts uppercase first letter in types (PascalCase), so
+      ;; this is the same as our conversion to C++ type name.
+      (otherwise (cpp-type-name cpp-type)))))
+
+(defun capnp-type-of-member (member)
+  (declare (type cpp-member member))
+  (if (cpp-member-capnp-type member)
+      (cpp-member-capnp-type member)
+      (capnp-type<-cpp-type (cpp-member-type member))))
+
+(defun capnp-primitive-type-p (capnp-type)
+  (declare (type (or list string) capnp-type))
+  (and (stringp capnp-type)
+       (member capnp-type
+               '("Bool"
+                 "Int8" "Int16" "Int32" "Int64"
+                 "UInt8" "UInt16" "UInt32" "UInt64"
+                 "Float32" "Float64"
+                 "Text" "Void")
+               :test #'string=)))
 
 (defun capnp-schema-for-enum (cpp-enum)
   "Generate Cap'n Proto serialization schema for CPP-ENUM"
   (declare (type cpp-enum cpp-enum))
   (with-output-to-string (s)
-    (format s "enum ~A {~%" (cpp-type-name (cpp-enum-symbol cpp-enum)))
+    (format s "enum ~A {~%" (cpp-type-name cpp-enum))
     (loop for val in (cpp-enum-values cpp-enum) and field-number from 0
        do (format s "  ~A @~A;~%"
                   (string-downcase (cpp-type-name val) :end 1)
@@ -630,14 +711,14 @@ encoded as union inheritance in Cap'n Proto."
   (declare (type (or cpp-class cpp-enum symbol) cpp-class))
   (when (null cpp-class)
     (return-from capnp-schema))
-  (when (cpp-enum-p cpp-class)
+  (when (typep cpp-class 'cpp-enum)
     (return-from capnp-schema (capnp-schema-for-enum cpp-class)))
-  (let ((class-name (if (symbolp cpp-class) cpp-class (cpp-class-name cpp-class)))
-        (members (when (cpp-class-p cpp-class) (cpp-class-members cpp-class)))
-        (inner-types (when (cpp-class-p cpp-class) (cpp-class-inner-types cpp-class)))
+  (let ((class-name (if (symbolp cpp-class) cpp-class (cpp-type-base-name cpp-class)))
+        (members (when (typep cpp-class 'cpp-class) (cpp-class-members cpp-class)))
+        (inner-types (when (typep cpp-class 'cpp-class) (cpp-class-inner-types cpp-class)))
         (union-subclasses (capnp-union-subclasses cpp-class))
-        (type-params (when (cpp-class-p cpp-class) (cpp-class-type-params cpp-class)))
-        (capnp-type-args (when (cpp-class-p cpp-class)
+        (type-params (when (typep cpp-class 'cpp-class) (cpp-type-type-params cpp-class)))
+        (capnp-type-args (when (typep cpp-class 'cpp-class)
                            (capnp-opts-type-args (cpp-class-capnp-opts cpp-class))))
         (field-number 0))
     (when (and type-params (not capnp-type-args))
@@ -667,22 +748,40 @@ encoded as union inheritance in Cap'n Proto."
               (incf field-number))
             (write-line "  }" s))
           (dolist (member members)
-            (format s "  ~A @~A :~A;~%"
-                    (field-name<-symbol (cpp-member-symbol member))
-                    field-number
-                    (if (cpp-member-capnp-type member)
-                        (cpp-member-capnp-type member)
-                        (capnp-type<-cpp-type (cpp-member-type member))))
-            (incf field-number))
+            (unless (eq :dont-save (cpp-member-capnp-save member))
+              (let ((capnp-type (capnp-type-of-member member))
+                    (field-name (field-name<-symbol (cpp-member-symbol member))))
+                (if (stringp capnp-type)
+                    (progn
+                      (format s "  ~A @~A :~A;~%"
+                              field-name field-number capnp-type)
+                      (incf field-number))
+                    ;; capnp-type is a list specifying a union type
+                    (progn
+                      (format s "  ~A :union {~%" field-name)
+                      (dolist (union-member capnp-type)
+                        (format s "    ~A @~A :~A;~%"
+                                (field-name<-symbol (first union-member))
+                                field-number (second union-member))
+                        (incf field-number))
+                      (write-line "  }" s))))))
           (dolist (inner inner-types)
-            (write-line (capnp-schema inner) s))
+            (when (or (and (typep inner 'cpp-class) (cpp-class-capnp-opts inner))
+                      (and (typep inner 'cpp-enum) (cpp-enum-capnp-schema inner)))
+              (write-line (capnp-schema inner) s)))
           (when union-subclasses
             (write-line "  union {" s)
+            (when union-parents
+              ;; Allow instantiating classes in the middle of inheritance
+              ;; hierarchy.
+              (format s "    ~A @~A :Void;~%"
+                      (field-name<-symbol class-name) field-number)
+              (incf field-number))
             (dolist (subclass union-subclasses)
               (format s "    ~A @~A :~A;~%"
-                      (field-name<-symbol (cpp-class-name subclass))
+                      (field-name<-symbol (cpp-type-base-name subclass))
                       field-number
-                      (capnp-type<-cpp-type (cpp-class-name subclass)))
+                      (capnp-type<-cpp-type (cpp-type-base-name subclass)))
               (incf field-number))
             (write-line "  }" s))
           (write-line "}" s))))))
@@ -744,7 +843,7 @@ encoded as union inheritance in Cap'n Proto."
   "Get additional arguments to Save/Load function for CPP-CLASS."
   (declare (type cpp-class cpp-class)
            (type (member :save :load) save-or-load))
-  (loop for parent in (cons (cpp-class-name cpp-class) (capnp-union-parents-rec cpp-class))
+  (loop for parent in (cons (cpp-type-base-name cpp-class) (capnp-union-parents-rec cpp-class))
      for opts = (cpp-class-capnp-opts (find-cpp-class parent))
      for args = (ecase save-or-load
                   (:save (capnp-opts-save-args opts))
@@ -758,8 +857,8 @@ used for outside definition."
   (declare (type cpp-class cpp-class))
   (let* ((parents (capnp-union-parents-rec cpp-class))
          (top-parent-class (if parents
-                               (cpp-class-full-name (find-cpp-class (car (last parents))))
-                               (cpp-class-full-name cpp-class)))
+                               (cpp-class-full-name (find-cpp-class (car (last parents))) :type-args nil)
+                               (cpp-class-full-name cpp-class :type-args nil)))
          (builder-arg
           (list (if parents 'base-builder 'builder)
                 (format nil "capnp::~A::Builder *" top-parent-class))))
@@ -772,7 +871,7 @@ used for outside definition."
   "Generate the default call to save for member."
   (declare (type string member-name member-type member-builder))
   (let* ((type (parse-cpp-type-declaration member-type))
-         (type-name (cpp-type-decl-name type)))
+         (type-name (cpp-type-base-name type)))
     (when (member type-name '("unique_ptr" "shared_ptr" "vector") :test #'string=)
       (error "Use a custom :capnp-save function for ~A ~A" type-name member-name))
     (let* ((cpp-class (find-cpp-class type-name)) ;; TODO: full type-name search
@@ -803,11 +902,15 @@ used for outside definition."
                       (cpp-type-name first-parent) (parent-args first-parent)))
             (when (or compose-parents (cpp-class-members cpp-class))
               (format s "  auto ~A_builder = base_builder->~{get~A().~}init~A();~%"
-                      (cpp-variable-name (cpp-class-name cpp-class))
+                      (cpp-variable-name (cpp-type-base-name cpp-class))
                       (mapcar #'cpp-type-name (cdr (reverse parents)))
-                      (cpp-type-name (cpp-class-name cpp-class)))
+                      (cpp-type-name cpp-class))
               (format s "  auto *builder = &~A_builder;~%"
-                      (cpp-variable-name (cpp-class-name cpp-class))))))
+                      (cpp-variable-name (cpp-type-base-name cpp-class))))
+            (when (capnp-union-subclasses cpp-class)
+              ;; We are in the middle of inheritance hierarchy, so set our
+              ;; union Void field.
+              (format s "  builder->set~A();" (cpp-type-name cpp-class)))))
         ;; Now handle composite inheritance calls.
         (dolist (parent compose-parents)
           (write-line "{" s)
@@ -818,39 +921,40 @@ used for outside definition."
           (write-line "}" s))))
     ;; Set the template instantiations
     (when (and (capnp-opts-type-args (cpp-class-capnp-opts cpp-class))
-               (/= 1 (list-length (cpp-class-type-params cpp-class))))
-      (error "Don't know how to save templated class ~A" (cpp-class-name cpp-class)))
-    (let ((type-param (first (cpp-class-type-params cpp-class))))
+               (/= 1 (list-length (cpp-type-type-params cpp-class))))
+      (error "Don't know how to save templated class ~A" (cpp-type-base-name cpp-class)))
+    (let ((type-param (first (cpp-type-type-params cpp-class))))
       (dolist (type-arg (capnp-opts-type-args (cpp-class-capnp-opts cpp-class)))
         (format s "  if (std::is_same<~A, ~A>::value) { builder->set~A(); }"
                 (cpp-type-name type-arg) (cpp-type-name type-param) (cpp-type-name type-arg))))
     (dolist (member (cpp-class-members cpp-class))
-      (let ((member-name (cpp-member-name member :struct (cpp-class-structp cpp-class)))
-            (member-builder (format nil "~A_builder" (cpp-member-name member :struct t)))
-            (capnp-name (cpp-type-name (cpp-member-symbol member))))
-        (cond
-          ((cpp-primitive-type-p (cpp-member-type member))
-           (format s "  builder->set~A(~A);~%" capnp-name member-name))
-          (t
-           (write-line "{" s) ;; Enclose larger save code in new scope
-           (let ((size (if (string= "vector" (cpp-type-decl-name
-                                              (parse-cpp-type-declaration
-                                               (cpp-member-type member))))
-                           (format nil "~A.size()" member-name)
-                           "")))
-             (if (cpp-member-capnp-init member)
-                 (format s "  auto ~A = builder->init~A(~A);~%"
-                         member-builder capnp-name size)
-                 (setf member-builder "builder")))
-           (if (cpp-member-capnp-save member)
-               (format s "  ~A~%"
-                       (cpp-code (funcall (cpp-member-capnp-save member)
-                                          member-builder member-name)))
-               (write-line (capnp-save-default member-name
-                                               (cpp-member-type member)
-                                               member-builder)
-                           s))
-           (write-line "}" s)))))
+      (unless (eq :dont-save (cpp-member-capnp-save member))
+        (let ((member-name (cpp-member-name member :struct (cpp-class-structp cpp-class)))
+              (member-builder (format nil "~A_builder" (cpp-member-name member :struct t)))
+              (capnp-name (cpp-type-name (cpp-member-symbol member))))
+          (cond
+            ((capnp-primitive-type-p (capnp-type-of-member member))
+             (format s "  builder->set~A(~A);~%" capnp-name member-name))
+            (t
+             (write-line "{" s) ;; Enclose larger save code in new scope
+             (let ((size (if (string= "vector" (cpp-type-base-name
+                                                (parse-cpp-type-declaration
+                                                 (cpp-member-type member))))
+                             (format nil "~A.size()" member-name)
+                             "")))
+               (if (cpp-member-capnp-init member)
+                   (format s "  auto ~A = builder->init~A(~A);~%"
+                           member-builder capnp-name size)
+                   (setf member-builder "builder")))
+             (if (cpp-member-capnp-save member)
+                 (format s "  ~A~%"
+                         (cpp-code (funcall (cpp-member-capnp-save member)
+                                            member-builder member-name)))
+                 (write-line (capnp-save-default member-name
+                                                 (cpp-member-type member)
+                                                 member-builder)
+                             s))
+             (write-line "}" s))))))
     (write-line "}" s)))
 
 ;;; Capnp C++ deserialization code generation
@@ -887,7 +991,7 @@ used for outside definition."
   (let ((construct-declaration (capnp-construct-declaration cpp-class :inline nil)))
     (unless construct-declaration
       (return-from capnp-construct-code))
-    (let ((class-name (cpp-class-name cpp-class))
+    (let ((class-name (cpp-type-base-name cpp-class))
           (union-subclasses (capnp-union-subclasses cpp-class)))
       (with-output-to-string (s)
         (format s "~A {~%" construct-declaration)
@@ -898,11 +1002,17 @@ used for outside definition."
             ;; Inheritance, so forward the Construct.
             (progn
               (write-line "  switch (reader.which()) {" s)
+              (when (capnp-union-and-compose-parents cpp-class)
+                ;; We are in the middle of the hierarchy, so allow
+                ;; constructing us.
+                (format s "    case capnp::~A::~A: return std::unique_ptr<~A>(new ~A());~%"
+                        (cpp-type-name class-name) (cpp-constant-name class-name)
+                        (cpp-type-name class-name) (cpp-type-name class-name)))
               (dolist (subclass union-subclasses)
                 (format s "    case capnp::~A::~A:~%"
                         (cpp-type-name class-name)
-                        (cpp-constant-name (cpp-class-name subclass)))
-                (let ((subclass-name (cpp-type-name (cpp-class-name subclass))))
+                        (cpp-constant-name (cpp-type-base-name subclass)))
+                (let ((subclass-name (cpp-type-name (cpp-type-base-name subclass))))
                   (if (capnp-opts-type-args (cpp-class-capnp-opts subclass))
                       ;; Handle template instantiation
                       (progn
@@ -923,7 +1033,7 @@ used for outside definition."
   "Generate default load call for member."
   (declare (type string member-name member-type member-reader))
   (let* ((type (parse-cpp-type-declaration member-type))
-         (type-name (cpp-type-decl-name type)))
+         (type-name (cpp-type-base-name type)))
     (when (member type-name '("unique_ptr" "shared_ptr" "vector") :test #'string=)
       (error "Use a custom :capnp-load function for ~A ~A" type-name member-name))
     (let* ((cpp-class (find-cpp-class type-name)) ;; TODO: full type-name search
@@ -941,8 +1051,8 @@ used for outside definition."
   (declare (type cpp-class cpp-class))
   (let* ((parents (capnp-union-parents-rec cpp-class))
          (top-parent-class (if parents
-                               (cpp-class-full-name (find-cpp-class (car (last parents))))
-                               (cpp-class-full-name cpp-class)))
+                               (cpp-class-full-name (find-cpp-class (car (last parents))) :type-args nil)
+                               (cpp-class-full-name cpp-class :type-args nil)))
          (reader-arg
           (list (if parents 'base-reader 'reader)
                 (format nil "const capnp::~A::Reader &" top-parent-class))))
@@ -972,7 +1082,7 @@ used for outside definition."
             (when (or compose-parents (cpp-class-members cpp-class))
               (format s "  auto reader = base_reader.~{get~A().~}get~A();~%"
                       (mapcar #'cpp-type-name (cdr (reverse parents)))
-                      (cpp-type-name (cpp-class-name cpp-class))))
+                      (cpp-type-name cpp-class)))
             ;; Now handle composite inheritance calls.
             (dolist (parent compose-parents)
               (write-line "{" s)
@@ -982,25 +1092,27 @@ used for outside definition."
                       (cpp-type-name parent) (cpp-variable-name parent) (parent-args parent))
               (write-line "}" s))))))
     (dolist (member (cpp-class-members cpp-class))
-      (let ((member-name (cpp-member-name member :struct (cpp-class-structp cpp-class)))
-            (member-reader (format nil "~A_reader" (cpp-member-name member :struct t)))
-            (capnp-name (cpp-type-name (cpp-member-symbol member))))
-        (cond
-          ((cpp-primitive-type-p (cpp-member-type member))
-           (format s "  ~A = reader.get~A();~%" member-name capnp-name))
-          (t
-           (write-line "{" s) ;; Enclose larger load code in new scope
-           (if (cpp-member-capnp-init member)
-               (format s "  auto ~A = reader.get~A();~%" member-reader capnp-name)
-               (setf member-reader "reader"))
-           (if (cpp-member-capnp-load member)
-               (format s "  ~A~%"
-                       (cpp-code (funcall (cpp-member-capnp-load member)
-                                          member-reader member-name)))
-               (write-line (capnp-load-default member-name
-                                               (cpp-member-type member)
-                                               member-reader) s))
-           (write-line "}" s)))))
+      (unless (and (eq :dont-save (cpp-member-capnp-save member))
+                   (not (cpp-member-capnp-load member)))
+        (let ((member-name (cpp-member-name member :struct (cpp-class-structp cpp-class)))
+              (member-reader (format nil "~A_reader" (cpp-member-name member :struct t)))
+              (capnp-name (cpp-type-name (cpp-member-symbol member))))
+          (cond
+            ((capnp-primitive-type-p (capnp-type-of-member member))
+             (format s "  ~A = reader.get~A();~%" member-name capnp-name))
+            (t
+             (write-line "{" s) ;; Enclose larger load code in new scope
+             (if (cpp-member-capnp-init member)
+                 (format s "  auto ~A = reader.get~A();~%" member-reader capnp-name)
+                 (setf member-reader "reader"))
+             (if (cpp-member-capnp-load member)
+                 (format s "  ~A~%"
+                         (cpp-code (funcall (cpp-member-capnp-load member)
+                                            member-reader member-name)))
+                 (write-line (capnp-load-default member-name
+                                                 (cpp-member-type member)
+                                                 member-reader) s))
+             (write-line "}" s))))))
     (write-line "}" s)))
 
 (defvar *capnp-imports* nil
@@ -1021,6 +1133,109 @@ used for outside definition."
   (declare (type string namespace))
   (setf *capnp-namespace* namespace))
 
+(defun capnp-save-optional (capnp-type cpp-type &optional lambda-code)
+  "Generate the C++ code calling utils::SaveOptional. CAPNP-TYPE and CPP-TYPE
+are passed as template parameters, while the optional LAMBDA-CODE is used to
+save the value inside the std::optional."
+  (declare (type string capnp-type cpp-type)
+           (type (or null string) lambda-code))
+  (let ((lambda-code (if lambda-code
+                         lambda-code
+                         "[](auto *builder, const auto &val) { val.Save(builder); }")))
+    (lambda (builder member)
+      #>cpp
+      utils::SaveOptional<${capnp-type}, ${cpp-type}>(${member}, &${builder}, ${lambda-code});
+      cpp<#)))
+
+(defun capnp-load-optional (capnp-type cpp-type &optional lambda-code)
+  "Generate the C++ code calling utils::LoadOptional. CAPNP-TYPE and CPP-TYPE
+are passed as template parameters, while the optional LAMBDA-CODE is used to
+load the value of std::optional."
+  (declare (type string capnp-type cpp-type)
+           (type (or null string) lambda-code))
+  (let ((lambda-code (if lambda-code
+                         lambda-code
+                         (format nil
+                                 "[](const auto &reader) { ~A val; val.Load(reader); return val; }"
+                                 cpp-type))))
+    (lambda (reader member)
+      #>cpp
+      ${member} = utils::LoadOptional<${capnp-type}, ${cpp-type}>(${reader}, ${lambda-code});
+      cpp<#)))
+
+(defun capnp-save-vector (capnp-type cpp-type &optional lambda-code)
+  "Generate the C++ code calling utils::SaveVector. CAPNP-TYPE and CPP-TYPE
+are passed as template parameters, while LAMBDA-CODE is used to save each
+element."
+  (declare (type string capnp-type cpp-type)
+           (type (or null string) lambda-code))
+  (let ((lambda-code (if lambda-code
+                         lambda-code
+                         "[](auto *builder, const auto &val) { val.Save(builder); }")))
+    (lambda (builder member-name)
+      #>cpp
+      utils::SaveVector<${capnp-type}, ${cpp-type}>(${member-name}, &${builder}, ${lambda-code});
+      cpp<#)))
+
+(defun capnp-load-vector (capnp-type cpp-type &optional lambda-code)
+  "Generate the C++ code calling utils::LoadVector. CAPNP-TYPE and CPP-TYPE
+are passed as template parameters, while LAMBDA-CODE is used to load each
+element."
+  (declare (type string capnp-type cpp-type)
+           (type (or null string) lambda-code))
+  (let ((lambda-code (if lambda-code
+                         lambda-code
+                         (format nil
+                                 "[](const auto &reader) { ~A val; val.Load(reader); return val; }"
+                                 cpp-type))))
+    (lambda (reader member-name)
+      #>cpp
+      utils::LoadVector<${capnp-type}, ${cpp-type}>(&${member-name}, ${reader}, ${lambda-code});
+      cpp<#)))
+
+(defun capnp-save-enum (capnp-type cpp-type &optional enum-values)
+  "Generate C++ code for saving the enum specified by CPP-TYPE by converting
+the values to CAPNP-TYPE. If ENUM-VALUES are not specified, tries to find the
+CPP-TYPE among defined enums."
+  (declare (type string capnp-type)
+           (type (or symbol string) cpp-type))
+  (lambda (builder member)
+    (let* ((enum-values (if enum-values
+                            enum-values
+                            (cpp-enum-values (find-cpp-enum cpp-type))))
+           (member-setter (remove #\_ (string-capitalize member)))
+           (cases (mapcar (lambda (value-symbol)
+                            (let ((value (cl-ppcre:regex-replace-all "-" (string value-symbol) "_")))
+                              #>cpp
+                              case ${cpp-type}::${value}:
+                                ${builder}->set${member-setter}(${capnp-type}::${value});
+                                break;
+                              cpp<#))
+                          enum-values)))
+      (format nil "switch (~A) {~%~{~A~%~}}" member (mapcar #'raw-cpp-string cases)))))
+
+(defun capnp-load-enum (capnp-type cpp-type &optional enum-values)
+  "Generate C++ code for loading the enum specified by CPP-TYPE by converting
+the values from CAPNP-TYPE. If ENUM-VALUES are not specified, tries to find the
+CPP-TYPE among defined enums."
+  (declare (type string capnp-type)
+           (type (or symbol string) cpp-type))
+  (lambda (reader member)
+    (let* ((enum-values (if enum-values
+                            enum-values
+                            (cpp-enum-values (find-cpp-enum cpp-type))))
+           (member-getter (remove #\_ (string-capitalize member)))
+           (cases (mapcar (lambda (value-symbol)
+                            (let ((value (cl-ppcre:regex-replace-all "-" (string value-symbol) "_")))
+                              #>cpp
+                              case ${capnp-type}::${value}:
+                                ${member} = ${cpp-type}::${value};
+                                break;
+                              cpp<#))
+                          enum-values)))
+      (format nil "switch (~A.get~A()) {~%~{~A~%~}}"
+              reader member-getter (mapcar #'raw-cpp-string cases)))))
+
 (defvar *cpp-namespaces* nil
   "Stack of C++ namespaces we are generating the code in.")
 
@@ -1037,30 +1252,39 @@ used for outside definition."
   (pop *cpp-namespaces*)
   #>cpp } cpp<#)
 
+(defvar *cpp-impl* nil "List of (namespace . C++ code) pairs that should be
+  written in the implementation (.cpp) file.")
+
+(defun in-impl (&rest args)
+  (let ((namespaces (reverse *cpp-namespaces*)))
+    (setf *cpp-impl*
+          (append *cpp-impl* (mapcar (lambda (cpp) (cons namespaces cpp))
+                                     args)))))
+
 (defvar *cpp-inner-types* nil
   "List of cpp types defined inside an enclosing class or struct")
 
 (defvar *cpp-enclosing-class* nil
   "Symbol name of the `CPP-CLASS' inside which inner types are defined.")
 
-(defmacro define-enum (name maybe-documentation &rest values)
-  "Define a C++ enum. Documentation is optional. Syntax is:
+(defmacro define-enum (name values &rest options)
+  "Define a C++ enum. Documentation is optional. The only options are
+  :documentation and :serialize. Syntax is:
 
 ;; (define-enum name
-;;   [documentation-string]
-;;   value1 value2 ...)"
-  (declare (type symbol name)
-           (type (or string symbol) maybe-documentation))
-  (let ((documentation (when (stringp maybe-documentation) maybe-documentation))
-        (all-values (if (symbolp maybe-documentation)
-                        (cons maybe-documentation values)
-                        values))
+;;   (value1 value2 ...)
+;;   (:enum-option option-value)*)"
+  (declare (type symbol name))
+  (let ((documentation (second (assoc :documentation options)))
         (enum (gensym (format nil "ENUM-~A" name))))
-    `(let ((,enum (make-cpp-enum :symbol ',name
+    `(let ((,enum (make-instance 'cpp-enum
+                                 :name ',name
                                  :documentation ,documentation
-                                 :values ',all-values
-                                 :enclosing-class *cpp-enclosing-class*)))
+                                 :values ',values
+                                 :enclosing-class *cpp-enclosing-class*
+                                 :capnp-schema ',(assoc :serialize options))))
        (prog1 ,enum
+         (push ,enum *cpp-enums*)
          (push ,enum *cpp-inner-types*)))))
 
 (defmacro define-class (name super-classes slots &rest options)
@@ -1147,26 +1371,27 @@ Generates C++:
         `(let ((,class
                 (let ((*cpp-inner-types* nil)
                       (*cpp-enclosing-class* ',class-name))
-                  (make-cpp-class :name ',class-name :super-classes ',super-classes
-                                  :type-params ',type-params
-                                  :structp ,(second (assoc :structp options))
-                                  :members (list ,@members)
-                                  :documentation ,(second (assoc :documentation options))
-                                  :public (list ,@(cdr (assoc :public options)))
-                                  :protected (list ,@(cdr (assoc :protected options)))
-                                  :private (list ,@(cdr (assoc :private options)))
-                                  :capnp-opts ,(when (member :capnp serialize)
-                                                 `(make-capnp-opts ,@(cdr (member :capnp serialize))))
-                                  :namespace (reverse *cpp-namespaces*)
-                                  ;; Set inner types at the end. This works
-                                  ;; because CL standard specifies order of
-                                  ;; evaluation from left to right.
-                                  :inner-types *cpp-inner-types*))))
+                  (make-instance 'cpp-class
+                                 :name ',class-name :super-classes ',super-classes
+                                 :type-params ',type-params
+                                 :structp ,(second (assoc :structp options))
+                                 :members (list ,@members)
+                                 :documentation ,(second (assoc :documentation options))
+                                 :public (list ,@(cdr (assoc :public options)))
+                                 :protected (list ,@(cdr (assoc :protected options)))
+                                 :private (list ,@(cdr (assoc :private options)))
+                                 :capnp-opts ,(when (member :capnp serialize)
+                                                `(make-capnp-opts ,@(cdr (member :capnp serialize))))
+                                 :namespace (reverse *cpp-namespaces*)
+                                 ;; Set inner types at the end. This works
+                                 ;; because CL standard specifies order of
+                                 ;; evaluation from left to right.
+                                 :inner-types *cpp-inner-types*))))
            (prog1 ,class
              (push ,class *cpp-classes*)
              ;; Set the parent's inner types
              (push ,class *cpp-inner-types*)
-             (setf (cpp-class-enclosing-class ,class) *cpp-enclosing-class*)
+             (setf (cpp-type-enclosing-class ,class) *cpp-enclosing-class*)
              ,(when (eq :boost (car serialize))
                 `(setf (cpp-class-private ,class)
                        (append (cpp-class-private ,class) (boost-serialization ,class))))))))))
@@ -1174,6 +1399,73 @@ Generates C++:
 (defmacro define-struct (name super-classes slots &rest options)
   `(define-class ,name ,super-classes ,slots (:structp t) ,@options))
 
+(defmacro define-rpc (name request response)
+  (declare (type list request response))
+  (assert (eq :request (car request)))
+  (assert (eq :response (car response)))
+  (flet ((decl-type-info (class-name)
+           #>cpp
+           using Capnp = capnp::${class-name};
+           static const communication::rpc::MessageType TypeInfo;
+           cpp<#)
+         (def-type-info (class-name)
+           #>cpp
+           const communication::rpc::MessageType
+           ${class-name}::TypeInfo{::capnp::typeId<${class-name}::Capnp>(), "${class-name}"};
+           cpp<#)
+         (def-constructor (class-name members)
+           (let ((full-constructor
+                  (let ((init-members (remove-if (lambda (slot-def)
+                                                   ;; TODO: proper initarg
+                                                   (let ((initarg (member :initarg slot-def)))
+                                                     (and initarg (null (second initarg)))))
+                                                 members)))
+                    (with-output-to-string (s)
+                      (when init-members
+                        (format s "~A ~A(~:{~A ~A~:^, ~}) : ~:{~A(~A)~:^, ~} {}"
+                                (if (= 1 (list-length init-members)) "explicit" "")
+                                class-name
+                                (mapcar (lambda (member)
+                                          (list (cpp-type-name (second member))
+                                                (cpp-variable-name (first member))))
+                                        init-members)
+                                (mapcar (lambda (member)
+                                          (let ((var (cpp-variable-name (first member)))
+                                                (movep (eq :move (second (member :initarg member)))))
+                                            (list var (if movep
+                                                          (format nil "std::move(~A)" var)
+                                                          var))))
+                                        init-members)))))))
+             #>cpp
+             ${class-name}() {}
+             ${full-constructor}
+             cpp<#)))
+    (let* ((req-sym (intern (format nil "~A-~A" name 'req)))
+           (req-name (cpp-type-name req-sym))
+           (res-sym (intern (format nil "~A-~A" name 'res)))
+           (res-name (cpp-type-name res-sym))
+           (rpc-name (format nil "~ARpc" (cpp-type-name name)))
+           (rpc-decl
+            #>cpp
+             using ${rpc-name} = communication::rpc::RequestResponse<${req-name}, ${res-name}>;
+             cpp<#))
+      `(cpp-list
+        (define-struct ,req-sym ()
+          ,@(cdr request)
+          (:public
+           ,(decl-type-info req-name)
+           ,(def-constructor req-name (second request)))
+          (:serialize :capnp :base t))
+        (in-impl ,(def-type-info req-name))
+        (define-struct ,res-sym ()
+          ,@(cdr response)
+          (:public
+           ,(decl-type-info res-name)
+           ,(def-constructor res-name (second response)))
+          (:serialize :capnp :base t))
+        (in-impl ,(def-type-info res-name))
+        ,rpc-decl))))
+
 (defun read-lcp (filepath)
   "Read the FILEPATH and return a list of C++ meta information that should be
 formatted and output."
@@ -1185,12 +1477,12 @@ formatted and output."
              for res = (handler-case (eval form)
                          (error (err)
                            (file-position in-stream 0) ;; start of stream
-                           (error "~%~A:~A: error:~%~%~A~%~%in:~%~%~A"
+                           (error "~%~A:~A: error:~2%~A~2%in:~2%~A"
                                   (uiop:native-namestring filepath)
                                   (count-newlines in-stream :stop-position (1+ stream-pos))
                                   err form)))
              do (setf stream-pos (file-position in-stream))
-             when (typep res '(or raw-cpp cpp-class cpp-enum))
+             when (typep res '(or raw-cpp cpp-type cpp-list))
              collect res)
         (end-of-file ()
           (file-position in-stream 0) ;; start of stream
@@ -1199,58 +1491,55 @@ formatted and output."
                  (count-newlines in-stream
                                  :stop-position (1+ stream-pos))))))))
 
-(defun generate-capnp (cpp-classes &key capnp-file capnp-id cpp-file hpp-file lcp-file)
-  "Generate Cap'n Proto serialization code for given CPP-CLASSES.  The schema
+(defun generate-capnp (cpp-types &key capnp-file capnp-id cpp-out lcp-file)
+  "Generate Cap'n Proto serialization code for given CPP-TYPES.  The schema
 is written to CAPNP-FILE using the CAPNP-ID.  The C++ serialization code is
-written to CPP-FILE.  This source file will include the provided HPP-FILE.
+written to CPP-OUT stream.  This source file will include the provided HPP-FILE.
 Original LCP-FILE is used just to insert a comment about the source of the
 code generation."
   (with-open-file (out capnp-file :direction :output :if-exists :supersede)
-    (format out "# Autogenerated using LCP from '~A'~%~%" lcp-file)
-    (format out "~A;~%~%" capnp-id)
+    (format out "# Autogenerated using LCP from '~A'~%# DO NOT EDIT!~2%" lcp-file)
+    (format out "~A;~2%" capnp-id)
     (write-line "using Cxx = import \"/capnp/c++.capnp\";" out)
-    (format out "$Cxx.namespace(\"~A::capnp\");~%~%" *capnp-namespace*)
+    (format out "$Cxx.namespace(\"~A::capnp\");~2%" *capnp-namespace*)
     (dolist (capnp-import *capnp-imports* (terpri out))
       (format out "using ~A = import ~S;~%"
               (remove #\- (string-capitalize (car capnp-import)))
               (cdr capnp-import)))
-    (dolist (cpp-class cpp-classes)
+    (dolist (cpp-type cpp-types)
       ;; Generate schema only for top level classes, inner classes are handled
       ;; inside the generation of the enclosing class.
-      (unless (cpp-class-enclosing-class cpp-class)
-        (let ((schema (capnp-schema cpp-class)))
+      (unless (cpp-type-enclosing-class cpp-type)
+        (let ((schema (capnp-schema cpp-type)))
           (when schema (write-line schema out))))))
   ;; Now generate the save/load C++ code in the cpp file.
-  (with-open-file (out cpp-file :direction :output :if-exists :supersede)
-    (format out "// Autogenerated using LCP from '~A'~%~%" lcp-file)
-    (write-line "#include \"utils/serialization.hpp\"" out)
-    (format out "#include \"~A\"~%~%" hpp-file)
-    (let (open-namespaces)
-      (dolist (cpp-class cpp-classes)
-        ;; Check if we need to open or close namespaces
-        (loop for namespace in (cpp-class-namespace cpp-class)
-           with unmatched = open-namespaces do
-             (if (string= namespace (car unmatched))
-                 (setf unmatched (cdr unmatched))
-                 (progn
-                   (dolist (to-close unmatched)
-                     (declare (ignore to-close))
-                     (write-line "}" out))
-                   (format out "namespace ~A {~%~%" namespace))))
-        (setf open-namespaces (cpp-class-namespace cpp-class))
-        ;; Output the serialization code
-        (format out "// Serialize code for ~A~%~%"
-                (cpp-type-name (cpp-class-name cpp-class)))
-        (let ((save-code (capnp-save-code cpp-class))
-              (construct-code (capnp-construct-code cpp-class))
-              (load-code (capnp-load-code cpp-class)))
-          (when save-code (write-line save-code out))
-          (when construct-code (write-line construct-code out))
-          (when load-code (write-line load-code out))))
-      ;; Close remaining namespaces
-      (dolist (to-close open-namespaces)
-        (declare (ignore to-close))
-        (write-line "}" out)))))
+  (write-line "// Autogenerated Cap'n Proto serialization code" cpp-out)
+  (write-line "#include \"utils/serialization.hpp\"" cpp-out)
+  (let (open-namespaces)
+    (dolist (cpp-class (remove-if (lambda (cpp-type) (not (typep cpp-type 'cpp-class))) cpp-types))
+      ;; Check if we need to open or close namespaces
+      (loop for namespace in (cpp-type-namespace cpp-class)
+         with unmatched = open-namespaces do
+           (if (string= namespace (car unmatched))
+               (setf unmatched (cdr unmatched))
+               (progn
+                 (dolist (to-close unmatched)
+                   (declare (ignore to-close))
+                   (format cpp-out "~%}"))
+                 (format cpp-out "namespace ~A {~2%" namespace))))
+      (setf open-namespaces (cpp-type-namespace cpp-class))
+      ;; Output the serialization code
+      (format cpp-out "// Serialize code for ~A~2%" (cpp-type-name cpp-class))
+      (let ((save-code (capnp-save-code cpp-class))
+            (construct-code (capnp-construct-code cpp-class))
+            (load-code (capnp-load-code cpp-class)))
+        (when save-code (write-line save-code cpp-out))
+        (when construct-code (write-line construct-code cpp-out))
+        (when load-code (write-line load-code cpp-out))))
+    ;; Close remaining namespaces
+    (dolist (to-close open-namespaces)
+      (declare (ignore to-close))
+      (format cpp-out "~%}"))))
 
 (defun process-file (lcp-file &key capnp-id)
   "Process a LCP-FILE and write the output to .hpp file in the same directory.
@@ -1270,24 +1559,52 @@ file."
           (*capnp-imports* nil)
           (*capnp-type-converters* nil)
           (*cpp-inner-types* nil)
+          (*cpp-impl*)
           ;; Don't reset *cpp-classes* if we want to have support for
           ;; procesing multiple files.
           ;; (*cpp-classes* nil)
+          ;; (*cpp-enums* nil)
           )
       ;; First read and evaluate the whole file, then output the evaluated
       ;; cpp-code. This allows us to generate code which may rely on
       ;; evaluation done after the code definition.
       (with-open-file (out hpp-file :direction :output :if-exists :supersede)
-        (format out "// Autogenerated using LCP from '~A'~%~%" lcp-file)
+        (format out "// Autogenerated using LCP from '~A'~%// DO NOT EDIT!~2%" lcp-file)
         (dolist (res (read-lcp lcp-file))
           (write-line (cpp-code res) out)))
       (when *cpp-namespaces*
         (error "Unclosed namespaces: ~A" (reverse *cpp-namespaces*)))
       ;; If we have a capnp-id, generate the schema
-      (when capnp-id
-        (let ((classes-for-capnp
-               (remove-if (complement #'cpp-class-capnp-opts) *cpp-classes*)))
-          (when classes-for-capnp
-            (generate-capnp classes-for-capnp :capnp-file capnp-file :capnp-id capnp-id
-                            :cpp-file cpp-file :hpp-file (file-namestring hpp-file)
-                            :lcp-file lcp-file)))))))
+      (let ((types-for-capnp (when capnp-id
+                               (append (remove-if (complement #'cpp-class-capnp-opts) *cpp-classes*)
+                                       (remove-if (complement #'cpp-enum-capnp-schema) *cpp-enums*)))))
+        ;; When we have either capnp or C++ code for the .cpp file, generate the .cpp file
+        (when (or *cpp-impl* types-for-capnp)
+          (with-open-file (out cpp-file :direction :output :if-exists :supersede)
+            (format out "// Autogenerated using LCP from '~A'~%// DO NOT EDIT!~2%" lcp-file)
+            (format out "#include \"~A\"~2%" (file-namestring hpp-file))
+            ;; First output the C++ code from the user
+            (let (open-namespaces)
+              (dolist (cpp *cpp-impl*)
+                (destructuring-bind (namespaces . code) cpp
+                  ;; Check if we need to open or close namespaces
+                  (loop for namespace in namespaces
+                     with unmatched = open-namespaces do
+                       (if (string= namespace (car unmatched))
+                           (setf unmatched (cdr unmatched))
+                           (progn
+                             (dolist (to-close unmatched)
+                               (declare (ignore to-close))
+                               (format out "~%}"))
+                             (format out "namespace ~A {~2%" namespace))))
+                  (setf open-namespaces namespaces)
+                  ;; Output the code
+                  (write-line (cpp-code code) out)))
+              ;; Close remaining namespaces
+              (dolist (to-close open-namespaces)
+                (declare (ignore to-close))
+                (format out "~%}")))
+            ;; Now output the capnp code
+            (when types-for-capnp
+              (generate-capnp types-for-capnp :capnp-file capnp-file :capnp-id capnp-id
+                              :cpp-out out :lcp-file lcp-file))))))))
diff --git a/src/query/frontend/ast/ast.capnp b/src/query/frontend/ast/ast.capnp
index 0f996e2a0..4269cc6c7 100644
--- a/src/query/frontend/ast/ast.capnp
+++ b/src/query/frontend/ast/ast.capnp
@@ -3,8 +3,8 @@
 using Cxx = import "/capnp/c++.capnp";
 $Cxx.namespace("query::capnp");
 
-using Utils = import "/utils/serialization.capnp";
-using Storage = import "/storage/types.capnp";
+using Dis = import "/distributed/serialization.capnp";
+using Storage = import "/storage/serialization.capnp";
 using Symbols = import "/query/frontend/semantic/symbol.capnp";
 
 struct Tree {
@@ -227,7 +227,7 @@ struct BaseLiteral {
 
 struct PrimitiveLiteral {
   tokenPosition @0 :Int32;
-  value @1 :Utils.TypedValue;
+  value @1 :Dis.TypedValue;
 }
 
 struct ListLiteral {
diff --git a/src/query/frontend/ast/ast.cpp b/src/query/frontend/ast/ast.cpp
index 154f59007..99cd17095 100644
--- a/src/query/frontend/ast/ast.cpp
+++ b/src/query/frontend/ast/ast.cpp
@@ -249,7 +249,7 @@ void PrimitiveLiteral::Save(capnp::BaseLiteral::Builder *base_literal_builder,
   auto primitive_literal_builder = base_literal_builder->initPrimitiveLiteral();
   primitive_literal_builder.setTokenPosition(token_position_);
   auto typed_value_builder = primitive_literal_builder.getValue();
-  utils::SaveCapnpTypedValue(value_, typed_value_builder);
+  utils::SaveCapnpTypedValue(value_, &typed_value_builder);
 }
 
 void PrimitiveLiteral::Load(const capnp::Tree::Reader &reader,
@@ -259,7 +259,7 @@ void PrimitiveLiteral::Load(const capnp::Tree::Reader &reader,
   auto pl_reader =
       reader.getExpression().getBaseLiteral().getPrimitiveLiteral();
   auto typed_value_reader = pl_reader.getValue();
-  utils::LoadCapnpTypedValue(value_, typed_value_reader);
+  utils::LoadCapnpTypedValue(typed_value_reader, &value_);
   token_position_ = pl_reader.getTokenPosition();
 }
 
diff --git a/src/query/frontend/semantic/symbol.capnp b/src/query/frontend/semantic/symbol.capnp
index 7d955391a..076ea08cb 100644
--- a/src/query/frontend/semantic/symbol.capnp
+++ b/src/query/frontend/semantic/symbol.capnp
@@ -19,3 +19,13 @@ struct Symbol {
   userDeclared @3 :Bool;
   tokenPosition @4 :Int32;
 }
+
+struct SymbolTable {
+  position @0 :Int32;
+  table @1 :List(Entry);
+
+  struct Entry {
+    key @0 :Int32;
+    val @1 :Symbol;
+  }
+}
diff --git a/src/query/frontend/semantic/symbol_table.hpp b/src/query/frontend/semantic/symbol_table.hpp
index 0499d4979..852572ff6 100644
--- a/src/query/frontend/semantic/symbol_table.hpp
+++ b/src/query/frontend/semantic/symbol_table.hpp
@@ -7,6 +7,7 @@
 #include "boost/serialization/serialization.hpp"
 
 #include "query/frontend/ast/ast.hpp"
+#include "query/frontend/semantic/symbol.capnp.h"
 #include "query/frontend/semantic/symbol.hpp"
 
 namespace query {
@@ -30,6 +31,29 @@ class SymbolTable final {
 
   const auto &table() const { return table_; }
 
+  void Save(capnp::SymbolTable::Builder *builder) const {
+    builder->setPosition(position_);
+    auto list_builder = builder->initTable(table_.size());
+    size_t i = 0;
+    for (const auto &entry : table_) {
+      auto entry_builder = list_builder[i++];
+      entry_builder.setKey(entry.first);
+      auto sym_builder = entry_builder.initVal();
+      entry.second.Save(&sym_builder);
+    }
+  }
+
+  void Load(const capnp::SymbolTable::Reader &reader) {
+    position_ = reader.getPosition();
+    table_.clear();
+    for (const auto &entry_reader : reader.getTable()) {
+      int key = entry_reader.getKey();
+      Symbol val;
+      val.Load(entry_reader.getVal());
+      table_[key] = val;
+    }
+  }
+
  private:
   int position_{0};
   std::map<int, Symbol> table_;
diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp
index 42ce06778..13f1cd87e 100644
--- a/src/query/plan/operator.cpp
+++ b/src/query/plan/operator.cpp
@@ -1345,10 +1345,10 @@ class DistributedExpandBfsCursor : public query::plan::Cursor {
   int current_depth_{-1};
 
   // Map from worker IDs to their corresponding subcursors.
-  std::unordered_map<int, int64_t> subcursor_ids_;
+  std::unordered_map<int16_t, int64_t> subcursor_ids_;
 
   // Next worker master should try pulling from.
-  std::unordered_map<int, int64_t>::iterator pull_pos_;
+  std::unordered_map<int16_t, int64_t>::iterator pull_pos_;
 };
 
 class ExpandWeightedShortestPathCursor : public query::plan::Cursor {
diff --git a/src/query/plan/operator.lcp b/src/query/plan/operator.lcp
index d6b2f9279..cc087ed51 100644
--- a/src/query/plan/operator.lcp
+++ b/src/query/plan/operator.lcp
@@ -140,7 +140,7 @@ cpp<#
 (lcp:capnp-namespace "query::plan")
 
 (lcp:capnp-import 'utils "/utils/serialization.capnp")
-(lcp:capnp-import 'storage "/storage/types.capnp")
+(lcp:capnp-import 'storage "/storage/serialization.capnp")
 (lcp:capnp-import 'ast "/query/frontend/ast/ast.capnp")
 (lcp:capnp-import 'semantic "/query/frontend/semantic/symbol.capnp")
 (lcp:capnp-import 'common "/query/common.capnp")
@@ -274,73 +274,6 @@ cpp<#
   LoadPointers(${archive}, ${member-name});
   cpp<#)
 
-(defun capnp-save-vector (capnp-type cpp-type &optional lambda-code)
-  (let ((lambda-code (if lambda-code
-                         lambda-code
-                         "[](auto *builder, const auto &val) { val.Save(builder); }")))
-    (lambda (builder member-name)
-      #>cpp
-      utils::SaveVector<${capnp-type}, ${cpp-type}>(${member-name}, &${builder}, ${lambda-code});
-      cpp<#)))
-
-(defun capnp-load-vector (capnp-type cpp-type &optional lambda-code)
-  (let ((lambda-code (if lambda-code
-                         lambda-code
-                         (format nil
-                                 "[](const auto &reader) { ~A val; val.Load(reader); return val; }"
-                                 cpp-type))))
-    (lambda (reader member-name)
-      #>cpp
-      utils::LoadVector<${capnp-type}, ${cpp-type}>(&${member-name}, ${reader}, ${lambda-code});
-      cpp<#)))
-
-(defun capnp-save-optional (capnp-type cpp-type &optional lambda-code)
-  (let ((lambda-code (if lambda-code
-                         lambda-code
-                         "[](auto *builder, const auto &val) { val.Save(builder); }")))
-    (lambda (builder member)
-      #>cpp
-      utils::SaveOptional<${capnp-type}, ${cpp-type}>(${member}, &${builder}, ${lambda-code});
-      cpp<#)))
-
-(defun capnp-load-optional (capnp-type cpp-type &optional lambda-code)
-  (let ((lambda-code (if lambda-code
-                         lambda-code
-                         (format nil
-                                 "[](const auto &reader) { ~A val; val.Load(reader); return val; }"
-                                 cpp-type))))
-    (lambda (reader member)
-      #>cpp
-      ${member} = utils::LoadOptional<${capnp-type}, ${cpp-type}>(${reader}, ${lambda-code});
-      cpp<#)))
-
-(defun capnp-save-enum (capnp-type cpp-type enum-values)
-  (lambda (builder member)
-    (let* ((member-setter (remove #\_ (string-capitalize member)))
-           (cases (mapcar (lambda (value-symbol)
-                           (let ((value (cl-ppcre:regex-replace-all "-" (string value-symbol) "_")))
-                             #>cpp
-                             case ${cpp-type}::${value}:
-                                 ${builder}->set${member-setter}(${capnp-type}::${value});
-                                 break;
-                                 cpp<#))
-                          enum-values)))
-      (format nil "switch (~A) {~%~{~A~%~}}" member (mapcar #'lcp::raw-cpp-string cases)))))
-
-(defun capnp-load-enum (capnp-type cpp-type enum-values)
-  (lambda (reader member)
-    (let* ((member-getter (remove #\_ (string-capitalize member)))
-           (cases (mapcar (lambda (value-symbol)
-                           (let ((value (cl-ppcre:regex-replace-all "-" (string value-symbol) "_")))
-                             #>cpp
-                             case ${capnp-type}::${value}:
-                                 ${member} = ${cpp-type}::${value};
-                                 break;
-                             cpp<#))
-                          enum-values)))
-      (format nil "switch (~A.get~A()) {~%~{~A~%~}}"
-              reader member-getter (mapcar #'lcp::raw-cpp-string cases)))))
-
 (defun save-ast-pointer (builder member)
   (let ((member-getter (remove #\_ (string-capitalize member))))
     #>cpp
@@ -361,19 +294,19 @@ cpp<#
       cpp<#)))
 
 (defun save-ast-vector (ast-type)
-  (capnp-save-vector "::query::capnp::Tree" ast-type
-                     "[helper](auto *builder, const auto &val) {
-                        val->Save(builder, &helper->saved_ast_uids);
-                      }"))
+  (lcp:capnp-save-vector "::query::capnp::Tree" ast-type
+                         "[helper](auto *builder, const auto &val) {
+                            val->Save(builder, &helper->saved_ast_uids);
+                          }"))
 
 (defun load-ast-vector (ast-type)
-  (capnp-load-vector "::query::capnp::Tree" ast-type
-                     (format
-                      nil
-                      "[helper](const auto &reader) {
-                        // We expect the unsafe downcast via static_cast to work.
-                        return static_cast<~A>(helper->ast_storage.Load(reader, &helper->loaded_ast_uids));
-                      }" ast-type)))
+  (lcp:capnp-load-vector "::query::capnp::Tree" ast-type
+                         (format
+                          nil
+                          "[helper](const auto &reader) {
+                            // We expect the unsafe downcast via static_cast to work.
+                            return static_cast<~A>(helper->ast_storage.Load(reader, &helper->loaded_ast_uids));
+                          }" ast-type)))
 
 (defun save-operator-pointer (builder member-name)
   #>cpp
@@ -586,10 +519,10 @@ chained in cases when longer paths need creating.
    (output-symbol "Symbol" :reader t :scope :protected)
    (graph-view "GraphView" :reader t :scope :protected
                :capnp-init nil
-               :capnp-save (capnp-save-enum "::query::capnp::GraphView" "GraphView"
-                                            '(old new))
-               :capnp-load (capnp-load-enum "::query::capnp::GraphView" "GraphView"
-                                            '(old new))
+               :capnp-save (lcp:capnp-save-enum "::query::capnp::GraphView" "GraphView"
+                                                '(old new))
+               :capnp-load (lcp:capnp-load-enum "::query::capnp::GraphView" "GraphView"
+                                                '(old new))
                :documentation
                "Controls which graph state is used to produce vertices.
 
@@ -661,8 +594,8 @@ given label.
            auto value_builder = builder->initValue();
            bound.value()->Save(&value_builder, &helper->saved_ast_uids);
          }"))
-    (funcall (capnp-save-optional "::utils::capnp::Bound<::query::capnp::Tree>" "Bound"
-                                  save-bound)
+    (funcall (lcp:capnp-save-optional "::utils::capnp::Bound<::query::capnp::Tree>" "Bound"
+                                      save-bound)
              builder member)))
 
 (defun load-optional-bound (reader member)
@@ -673,8 +606,8 @@ given label.
            auto *value = static_cast<Expression*>(helper->ast_storage.Load(reader.getValue(), &helper->loaded_ast_uids));
            return Bound(value, type);
          }"))
-    (funcall (capnp-load-optional "::utils::capnp::Bound<::query::capnp::Tree>" "Bound"
-                                  load-bound)
+    (funcall (lcp:capnp-load-optional "::utils::capnp::Bound<::query::capnp::Tree>" "Bound"
+                                      load-bound)
              reader member)))
 
 (lcp:define-class scan-all-by-label-property-range (scan-all)
@@ -805,13 +738,13 @@ property value.
    (edge-symbol "Symbol" :reader t :scope :protected)
    (direction "EdgeAtom::Direction" :reader t :scope :protected
               :capnp-type "Ast.EdgeAtom.Direction" :capnp-init nil
-              :capnp-save (capnp-save-enum "::query::capnp::EdgeAtom::Direction" "EdgeAtom::Direction"
-                                           '(in out both))
-              :capnp-load (capnp-load-enum "::query::capnp::EdgeAtom::Direction" "EdgeAtom::Direction"
-                                           '(in out both)))
+              :capnp-save (lcp:capnp-save-enum "::query::capnp::EdgeAtom::Direction" "EdgeAtom::Direction"
+                                               '(in out both))
+              :capnp-load (lcp:capnp-load-enum "::query::capnp::EdgeAtom::Direction" "EdgeAtom::Direction"
+                                               '(in out both)))
    (edge-types "std::vector<storage::EdgeType>" :reader t :scope :protected
-               :capnp-save (capnp-save-vector "::storage::capnp::Common" "storage::EdgeType")
-               :capnp-load (capnp-load-vector "::storage::capnp::Common" "storage::EdgeType"))
+               :capnp-save (lcp:capnp-save-vector "::storage::capnp::Common" "storage::EdgeType")
+               :capnp-load (lcp:capnp-load-vector "::storage::capnp::Common" "storage::EdgeType"))
    ;; the input op and the symbol under which the op's result
    ;; can be found in the frame
    (input "std::shared_ptr<LogicalOperator>" :scope :protected
@@ -823,10 +756,10 @@ property value.
                   been expanded and should be just validated in the frame.")
    (graph-view "GraphView" :reader t :scope :protected
                :capnp-init nil
-               :capnp-save (capnp-save-enum "::query::capnp::GraphView" "GraphView"
-                                            '(old new))
-               :capnp-load (capnp-load-enum "::query::capnp::GraphView" "GraphView"
-                                            '(old new))
+               :capnp-save (lcp:capnp-save-enum "::query::capnp::GraphView" "GraphView"
+                                                '(old new))
+               :capnp-load (lcp:capnp-load-enum "::query::capnp::GraphView" "GraphView"
+                                                '(old new))
                :documentation
                "from which state the input node should get expanded"))
   (:documentation
@@ -969,10 +902,10 @@ pulled.")
 (lcp:define-class expand-variable (logical-operator expand-common)
   ((type "EdgeAtom::Type" :reader t :capnp-type "Ast.EdgeAtom.Type"
          :capnp-init nil
-         :capnp-save (capnp-save-enum "::query::capnp::EdgeAtom::Type" "EdgeAtom::Type"
-                                      '(single depth-first breadth-first weighted-shortest-path))
-         :capnp-load (capnp-load-enum "::query::capnp::EdgeAtom::Type" "EdgeAtom::Type"
-                                      '(single depth-first breadth-first weighted-shortest-path)))
+         :capnp-save (lcp:capnp-save-enum "::query::capnp::EdgeAtom::Type" "EdgeAtom::Type"
+                                          '(single depth-first breadth-first weighted-shortest-path))
+         :capnp-load (lcp:capnp-load-enum "::query::capnp::EdgeAtom::Type" "EdgeAtom::Type"
+                                          '(single depth-first breadth-first weighted-shortest-path)))
    (is-reverse :bool :documentation
                "True if the path should be written as expanding from node_symbol to input_symbol.")
    (lower-bound "Expression *" :save-fun #'save-pointer :load-fun #'load-pointer
@@ -985,15 +918,15 @@ pulled.")
                 :documentation "Optional upper bound of the variable length expansion, defaults are (1, inf)")
    (filter-lambda "Lambda")
    (weight-lambda "std::experimental::optional<Lambda>"
-                  :capnp-save (capnp-save-optional
+                  :capnp-save (lcp:capnp-save-optional
                                "capnp::ExpandVariable::Lambda" "Lambda"
                                "[helper](auto *builder, const auto &val) { val.Save(builder, helper); }")
-                  :capnp-load (capnp-load-optional
+                  :capnp-load (lcp:capnp-load-optional
                                "capnp::ExpandVariable::Lambda" "Lambda"
                                "[helper](const auto &reader) { Lambda val; val.Load(reader, helper); return val; }"))
    (total-weight "std::experimental::optional<Symbol>"
-                 :capnp-save (capnp-save-optional "::query::capnp::Symbol" "Symbol")
-                 :capnp-load (capnp-load-optional "::query::capnp::Symbol" "Symbol")))
+                 :capnp-save (lcp:capnp-save-optional "::query::capnp::Symbol" "Symbol")
+                 :capnp-load (lcp:capnp-load-optional "::query::capnp::Symbol" "Symbol")))
   (:documentation
    "Variable-length expansion operator. For a node existing in
 the frame it expands a variable number of edges and places them
@@ -1088,8 +1021,8 @@ pulled.")
           :capnp-load #'load-operator-pointer)
    (path-symbol "Symbol" :reader t)
    (path-elements "std::vector<Symbol>" :reader t
-                  :capnp-save (capnp-save-vector "::query::capnp::Symbol" "Symbol")
-                  :capnp-load (capnp-load-vector "::query::capnp::Symbol" "Symbol")))
+                  :capnp-save (lcp:capnp-save-vector "::query::capnp::Symbol" "Symbol")
+                  :capnp-load (lcp:capnp-load-vector "::query::capnp::Symbol" "Symbol")))
   (:documentation
    "Constructs a named path from it's elements and places it on the frame.")
   (:public
@@ -1324,8 +1257,8 @@ can be stored (a TypedValue that can be converted to PropertyValue).")
         :capnp-save #'save-ast-pointer :capnp-load (load-ast-pointer "Expression *")
         :save-fun #'save-pointer :load-fun #'load-pointer)
    (op "Op" :capnp-init nil
-       :capnp-save (capnp-save-enum "capnp::SetProperties::Op" "Op" '(update replace))
-       :capnp-load (capnp-load-enum "capnp::SetProperties::Op" "Op" '(update replace))))
+       :capnp-save (lcp:capnp-save-enum "capnp::SetProperties::Op" "Op")
+       :capnp-load (lcp:capnp-load-enum "capnp::SetProperties::Op" "Op")))
   (:documentation
    "Logical op for setting the whole properties set on a vertex or an edge.
 
@@ -1336,12 +1269,13 @@ Supports setting (replacing the whole properties set with another) and
 updating.")
   (:public
    (lcp:define-enum op
-       "Defines how setting the properties works.
+     (update replace)
+     (:documentation "Defines how setting the properties works.
 
 @c UPDATE means that the current property set is augmented with additional
 ones (existing props of the same name are replaced), while @c REPLACE means
-that the old props are discarded and replaced with new ones."
-     update replace)
+that the old props are discarded and replaced with new ones.")
+     (:serialize :capnp))
 
    #>cpp
    SetProperties(const std::shared_ptr<LogicalOperator> &input,
@@ -1390,8 +1324,8 @@ that the old props are discarded and replaced with new ones."
           :capnp-load #'load-operator-pointer)
    (input-symbol "Symbol")
    (labels "std::vector<storage::Label>"
-     :capnp-save (capnp-save-vector "::storage::capnp::Common" "storage::Label")
-     :capnp-load (capnp-load-vector "::storage::capnp::Common" "storage::Label")))
+     :capnp-save (lcp:capnp-save-vector "::storage::capnp::Common" "storage::Label")
+     :capnp-load (lcp:capnp-load-vector "::storage::capnp::Common" "storage::Label")))
   (:documentation
    "Logical operator for setting an arbitrary number of labels on a Vertex.
 
@@ -1478,8 +1412,8 @@ It does NOT remove labels that are already set on that Vertex.")
           :capnp-load #'load-operator-pointer)
    (input-symbol "Symbol")
    (labels "std::vector<storage::Label>"
-     :capnp-save (capnp-save-vector "::storage::capnp::Common" "storage::Label")
-     :capnp-load (capnp-load-vector "::storage::capnp::Common" "storage::Label")))
+     :capnp-save (lcp:capnp-save-vector "::storage::capnp::Common" "storage::Label")
+     :capnp-load (lcp:capnp-load-vector "::storage::capnp::Common" "storage::Label")))
   (:documentation
    "Logical operator for removing an arbitrary number of labels on a Vertex.
 
@@ -1522,8 +1456,8 @@ If a label does not exist on a Vertex, nothing happens.")
           :capnp-load #'load-operator-pointer)
    (expand-symbol "Symbol")
    (previous-symbols "std::vector<Symbol>"
-                     :capnp-save (capnp-save-vector "::query::capnp::Symbol" "Symbol")
-                     :capnp-load (capnp-load-vector "::query::capnp::Symbol" "Symbol")))
+                     :capnp-save (lcp:capnp-save-vector "::query::capnp::Symbol" "Symbol")
+                     :capnp-load (lcp:capnp-load-vector "::query::capnp::Symbol" "Symbol")))
   (:documentation
    "Filter whose Pull returns true only when the given
 expand_symbol frame value (the latest expansion) is not
@@ -1585,8 +1519,8 @@ between edges and an edge lists).")
           :capnp-save #'save-operator-pointer
           :capnp-load #'load-operator-pointer)
    (symbols "std::vector<Symbol>" :reader t
-            :capnp-save (capnp-save-vector "::query::capnp::Symbol" "Symbol")
-            :capnp-load (capnp-load-vector "::query::capnp::Symbol" "Symbol"))
+            :capnp-save (lcp:capnp-save-vector "::query::capnp::Symbol" "Symbol")
+            :capnp-load (lcp:capnp-load-vector "::query::capnp::Symbol" "Symbol"))
    (advance-command :bool :reader t))
   (:documentation
    "Pulls everything from the input before passing it through.
@@ -1667,10 +1601,10 @@ cpp<#
           :capnp-save #'save-operator-pointer
           :capnp-load #'load-operator-pointer)
    (aggregations "std::vector<Element>" :reader t
-                 :capnp-save (capnp-save-vector
+                 :capnp-save (lcp:capnp-save-vector
                               "capnp::Aggregate::Element" "Element"
                               "[helper](auto *builder, const auto &val) { val.Save(builder, helper); }")
-                 :capnp-load (capnp-load-vector
+                 :capnp-load (lcp:capnp-load-vector
                               "capnp::Aggregate::Element" "Element"
                               "[helper](const auto &reader) { Element val; val.Load(reader, helper); return val; }"))
    (group-by "std::vector<Expression *>" :reader t
@@ -1679,8 +1613,8 @@ cpp<#
              :capnp-load (load-ast-vector "Expression *")
              :save-fun #'save-pointers :load-fun #'load-pointers)
    (remember "std::vector<Symbol>" :reader t
-             :capnp-save (capnp-save-vector "::query::capnp::Symbol" "Symbol")
-             :capnp-load (capnp-load-vector "::query::capnp::Symbol" "Symbol")))
+             :capnp-save (lcp:capnp-save-vector "::query::capnp::Symbol" "Symbol")
+             :capnp-load (lcp:capnp-load-vector "::query::capnp::Symbol" "Symbol")))
   (:documentation
    "Performs an arbitrary number of aggregations of data
 from the given input grouped by the given criteria.
@@ -1706,10 +1640,10 @@ elements are in an undefined state after aggregation.")
            :capnp-save #'save-ast-pointer :capnp-load (load-ast-pointer "Expression *")
            :save-fun #'save-pointer :load-fun #'load-pointer)
       (op "Aggregation::Op" :capnp-type "Ast.Aggregation.Op"
-          :capnp-init nil :capnp-save (capnp-save-enum "::query::capnp::Aggregation::Op" "Aggregation::Op"
-                                                       '(count min max sum avg collect-list collect-map))
-          :capnp-load (capnp-load-enum "::query::capnp::Aggregation::Op" "Aggregation::Op"
-                                       '(count min max sum avg collect-list collect-map)))
+          :capnp-init nil :capnp-save (lcp:capnp-save-enum "::query::capnp::Aggregation::Op" "Aggregation::Op"
+                                                           '(count min max sum avg collect-list collect-map))
+          :capnp-load (lcp:capnp-load-enum "::query::capnp::Aggregation::Op" "Aggregation::Op"
+                                           '(count min max sum avg collect-list collect-map)))
       (output-sym "Symbol"))
      (:documentation
       "An aggregation element, contains:
@@ -1949,8 +1883,8 @@ input should be performed).")
              :capnp-load (load-ast-vector "Expression *")
              :save-fun #'save-pointers :load-fun #'load-pointers)
    (output-symbols "std::vector<Symbol>" :reader t
-                   :capnp-save (capnp-save-vector "::query::capnp::Symbol" "Symbol")
-                   :capnp-load (capnp-load-vector "::query::capnp::Symbol" "Symbol")))
+                   :capnp-save (lcp:capnp-save-vector "::query::capnp::Symbol" "Symbol")
+                   :capnp-load (lcp:capnp-load-vector "::query::capnp::Symbol" "Symbol")))
   (:documentation
    "Logical operator for ordering (sorting) results.
 
@@ -2080,8 +2014,8 @@ documentation.")
              :capnp-save #'save-operator-pointer
              :capnp-load #'load-operator-pointer)
    (optional-symbols "std::vector<Symbol>" :reader t
-                     :capnp-save (capnp-save-vector "::query::capnp::Symbol" "Symbol")
-                     :capnp-load (capnp-load-vector "::query::capnp::Symbol" "Symbol")))
+                     :capnp-save (lcp:capnp-save-vector "::query::capnp::Symbol" "Symbol")
+                     :capnp-load (lcp:capnp-load-vector "::query::capnp::Symbol" "Symbol")))
   (:documentation
    "Optional operator. Used for optional match. For every
 successful Pull from the input branch a Pull from the optional
@@ -2186,8 +2120,8 @@ Input is optional (unwind can be the first clause in a query).")
           :capnp-save #'save-operator-pointer
           :capnp-load #'load-operator-pointer)
    (value-symbols "std::vector<Symbol>"
-                  :capnp-save (capnp-save-vector "::query::capnp::Symbol" "Symbol")
-                  :capnp-load (capnp-load-vector "::query::capnp::Symbol" "Symbol")))
+                  :capnp-save (lcp:capnp-save-vector "::query::capnp::Symbol" "Symbol")
+                  :capnp-load (lcp:capnp-load-vector "::query::capnp::Symbol" "Symbol")))
   (:documentation
    "Ensures that only distinct rows are yielded.
 This implementation accepts a vector of Symbols
@@ -2272,14 +2206,14 @@ case the index already exists, nothing happens.")
              :capnp-save #'save-operator-pointer
              :capnp-load #'load-operator-pointer)
    (union-symbols "std::vector<Symbol>" :reader t
-                  :capnp-save (capnp-save-vector "::query::capnp::Symbol" "Symbol")
-                  :capnp-load (capnp-load-vector "::query::capnp::Symbol" "Symbol"))
+                  :capnp-save (lcp:capnp-save-vector "::query::capnp::Symbol" "Symbol")
+                  :capnp-load (lcp:capnp-load-vector "::query::capnp::Symbol" "Symbol"))
    (left-symbols "std::vector<Symbol>" :reader t
-                 :capnp-save (capnp-save-vector "::query::capnp::Symbol" "Symbol")
-                 :capnp-load (capnp-load-vector "::query::capnp::Symbol" "Symbol"))
+                 :capnp-save (lcp:capnp-save-vector "::query::capnp::Symbol" "Symbol")
+                 :capnp-load (lcp:capnp-load-vector "::query::capnp::Symbol" "Symbol"))
    (right-symbols "std::vector<Symbol>" :reader t
-                  :capnp-save (capnp-save-vector "::query::capnp::Symbol" "Symbol")
-                  :capnp-load (capnp-load-vector "::query::capnp::Symbol" "Symbol")))
+                  :capnp-save (lcp:capnp-save-vector "::query::capnp::Symbol" "Symbol")
+                  :capnp-load (lcp:capnp-load-vector "::query::capnp::Symbol" "Symbol")))
   (:documentation
    "A logical operator that applies UNION operator on inputs and places the
 result on the frame.
@@ -2326,8 +2260,8 @@ vectors of symbols used  by each of the inputs.")
           :capnp-load #'load-operator-pointer)
    (plan-id :int64_t :initval 0 :reader t)
    (symbols "std::vector<Symbol>" :reader t
-            :capnp-save (capnp-save-vector "::query::capnp::Symbol" "Symbol")
-            :capnp-load (capnp-load-vector "::query::capnp::Symbol" "Symbol")))
+            :capnp-save (lcp:capnp-save-vector "::query::capnp::Symbol" "Symbol")
+            :capnp-load (lcp:capnp-load-vector "::query::capnp::Symbol" "Symbol")))
   (:documentation
    "An operator in distributed Memgraph that yields both local and remote (from
 other workers) frames. Obtaining remote frames is done through RPC calls to
@@ -2424,14 +2358,14 @@ Logic of the synchronize operator is:
             :capnp-save #'save-operator-pointer
             :capnp-load #'load-operator-pointer)
    (left-symbols "std::vector<Symbol>" :reader t
-                 :capnp-save (capnp-save-vector "::query::capnp::Symbol" "Symbol")
-                 :capnp-load (capnp-load-vector "::query::capnp::Symbol" "Symbol"))
+                 :capnp-save (lcp:capnp-save-vector "::query::capnp::Symbol" "Symbol")
+                 :capnp-load (lcp:capnp-load-vector "::query::capnp::Symbol" "Symbol"))
    (right-op "std::shared_ptr<LogicalOperator>" :reader t
              :capnp-save #'save-operator-pointer
              :capnp-load #'load-operator-pointer)
    (right-symbols "std::vector<Symbol>" :reader t
-                  :capnp-save (capnp-save-vector "::query::capnp::Symbol" "Symbol")
-                  :capnp-load (capnp-load-vector "::query::capnp::Symbol" "Symbol")))
+                  :capnp-save (lcp:capnp-save-vector "::query::capnp::Symbol" "Symbol")
+                  :capnp-load (lcp:capnp-load-vector "::query::capnp::Symbol" "Symbol")))
   (:documentation
    "Operator for producing a Cartesian product from 2 input branches")
   (:public
@@ -2464,8 +2398,8 @@ Logic of the synchronize operator is:
           :capnp-load #'load-operator-pointer)
    (plan-id :int64_t :initval 0 :reader t)
    (symbols "std::vector<Symbol>" :reader t
-            :capnp-save (capnp-save-vector "::query::capnp::Symbol" "Symbol")
-            :capnp-load (capnp-load-vector "::query::capnp::Symbol" "Symbol"))
+            :capnp-save (lcp:capnp-save-vector "::query::capnp::Symbol" "Symbol")
+            :capnp-load (lcp:capnp-load-vector "::query::capnp::Symbol" "Symbol"))
    (order-by "std::vector<Expression *>" :reader t
              :capnp-type "List(Ast.Tree)"
              :capnp-save (save-ast-vector "Expression *")
diff --git a/src/stats/stats_rpc_messages.hpp b/src/stats/stats_rpc_messages.hpp
deleted file mode 100644
index b5106097c..000000000
--- a/src/stats/stats_rpc_messages.hpp
+++ /dev/null
@@ -1,62 +0,0 @@
-#pragma once
-
-#include "boost/serialization/access.hpp"
-#include "boost/serialization/base_object.hpp"
-#include "boost/serialization/string.hpp"
-#include "boost/serialization/utility.hpp"
-#include "boost/serialization/vector.hpp"
-
-#include "communication/rpc/messages.hpp"
-#include "utils/timestamp.hpp"
-
-namespace stats {
-
-struct StatsReq : public communication::rpc::Message {
-  StatsReq() {}
-  StatsReq(std::string metric_path,
-           std::vector<std::pair<std::string, std::string>> tags, double value)
-      : metric_path(metric_path),
-        tags(tags),
-        value(value),
-        timestamp(utils::Timestamp::Now().SecSinceTheEpoch()) {}
-
-  std::string metric_path;
-  std::vector<std::pair<std::string, std::string>> tags;
-  double value;
-  uint64_t timestamp;
-
- private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<communication::rpc::Message>(*this);
-    ar &metric_path &tags &value &timestamp;
-  }
-};
-
-RPC_NO_MEMBER_MESSAGE(StatsRes);
-
-struct BatchStatsReq : public communication::rpc::Message {
-  BatchStatsReq() {}
-  explicit BatchStatsReq(std::vector<StatsReq> requests) : requests(requests) {}
-
-  std::vector<StatsReq> requests;
-
- private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<communication::rpc::Message>(*this);
-    ar &requests;
-  }
-};
-
-RPC_NO_MEMBER_MESSAGE(BatchStatsRes);
-
-using StatsRpc = communication::rpc::RequestResponse<StatsReq, StatsRes>;
-using BatchStatsRpc =
-    communication::rpc::RequestResponse<BatchStatsReq, BatchStatsRes>;
-
-}  // namespace stats
diff --git a/src/stats/stats_rpc_messages.lcp b/src/stats/stats_rpc_messages.lcp
new file mode 100644
index 000000000..87e2fcac4
--- /dev/null
+++ b/src/stats/stats_rpc_messages.lcp
@@ -0,0 +1,50 @@
+#>cpp
+#pragma once
+
+#include "communication/rpc/messages.hpp"
+#include "stats/stats_rpc_messages.capnp.h"
+#include "utils/serialization.hpp"
+#include "utils/timestamp.hpp"
+cpp<#
+
+(lcp:namespace stats)
+
+(lcp:capnp-namespace "stats")
+
+(lcp:capnp-import 'utils "/utils/serialization.capnp")
+
+(lcp:define-rpc stats
+    (:request
+     ((metric-path "std::string")
+      (tags "std::vector<std::pair<std::string, std::string>>"
+            :capnp-type "List(Utils.Pair(Text, Text))"
+            :capnp-save
+            (lcp:capnp-save-vector
+             "utils::capnp::Pair<::capnp::Text, ::capnp::Text>"
+             "std::pair<std::string, std::string>"
+             "[](auto *builder, const auto &pair) {
+                builder->setFirst(pair.first);
+                builder->setSecond(pair.second);
+              }")
+            :capnp-load
+            (lcp:capnp-load-vector
+             "utils::capnp::Pair<::capnp::Text, ::capnp::Text>"
+             "std::pair<std::string, std::string>"
+             "[](const auto &reader) {
+               std::string first = reader.getFirst();
+               std::string second = reader.getSecond();
+               return std::make_pair(first, second);
+             }"))
+      (value :double)
+      (timestamp :uint64_t :initarg nil)))
+  (:response ()))
+
+(lcp:define-rpc batch-stats
+    (:request
+     ((requests "std::vector<StatsReq>"
+                :capnp-type "List(StatsReq)"
+                :capnp-save (lcp:capnp-save-vector "capnp::StatsReq" "StatsReq")
+                :capnp-load (lcp:capnp-load-vector "capnp::StatsReq" "StatsReq"))))
+  (:response ()))
+
+(lcp:pop-namespace) ;; stats
diff --git a/src/storage/address.hpp b/src/storage/address.hpp
index 93e83dcba..ff6d7bd8a 100644
--- a/src/storage/address.hpp
+++ b/src/storage/address.hpp
@@ -2,9 +2,9 @@
 
 #include <cstdint>
 
-#include "boost/serialization/access.hpp"
 #include "glog/logging.h"
 
+#include "storage/serialization.capnp.h"
 #include "storage/gid.hpp"
 
 namespace storage {
@@ -89,13 +89,16 @@ class Address {
     return storage_ == other.storage_;
   }
 
+  void Save(capnp::Address::Builder *builder) const {
+    builder->setStorage(storage_);
+  }
+
+  void Load(const capnp::Address::Reader &reader) {
+    storage_ = reader.getStorage();
+  }
+
  private:
   StorageT storage_{0};
-
-  friend class boost::serialization::access;
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &storage_;
-  }
 };
+
 }  // namespace storage
diff --git a/src/storage/concurrent_id_mapper_master.cpp b/src/storage/concurrent_id_mapper_master.cpp
index d8df45f5a..aa4799581 100644
--- a/src/storage/concurrent_id_mapper_master.cpp
+++ b/src/storage/concurrent_id_mapper_master.cpp
@@ -10,16 +10,24 @@ namespace {
 template <typename TId>
 void RegisterRpc(MasterConcurrentIdMapper<TId> &mapper,
                  communication::rpc::Server &rpc_server);
-#define ID_VALUE_RPC_CALLS(type)                                              \
-  template <>                                                                 \
-  void RegisterRpc<type>(MasterConcurrentIdMapper<type> & mapper,             \
-                         communication::rpc::Server & rpc_server) {           \
-    rpc_server.Register<type##IdRpc>([&mapper](const type##IdReq &req) {      \
-      return std::make_unique<type##IdRes>(mapper.value_to_id(req.member));   \
-    });                                                                       \
-    rpc_server.Register<Id##type##Rpc>([&mapper](const Id##type##Req &req) {  \
-      return std::make_unique<Id##type##Res>(mapper.id_to_value(req.member)); \
-    });                                                                       \
+#define ID_VALUE_RPC_CALLS(type)                                    \
+  template <>                                                       \
+  void RegisterRpc<type>(MasterConcurrentIdMapper<type> & mapper,   \
+                         communication::rpc::Server & rpc_server) { \
+    rpc_server.Register<type##IdRpc>(                               \
+        [&mapper](const auto &req_reader, auto *res_builder) {      \
+          type##IdReq req;                                          \
+          req.Load(req_reader);                                     \
+          type##IdRes res(mapper.value_to_id(req.member));          \
+          res.Save(res_builder);                                    \
+        });                                                         \
+    rpc_server.Register<Id##type##Rpc>(                             \
+        [&mapper](const auto &req_reader, auto *res_builder) {      \
+          Id##type##Req req;                                        \
+          req.Load(req_reader);                                     \
+          Id##type##Res res(mapper.id_to_value(req.member));        \
+          res.Save(res_builder);                                    \
+        });                                                         \
   }
 
 using namespace storage;
diff --git a/src/storage/concurrent_id_mapper_rpc_messages.hpp b/src/storage/concurrent_id_mapper_rpc_messages.hpp
deleted file mode 100644
index 06e1d7f87..000000000
--- a/src/storage/concurrent_id_mapper_rpc_messages.hpp
+++ /dev/null
@@ -1,29 +0,0 @@
-#pragma once
-
-#include <chrono>
-
-#include "communication/rpc/messages.hpp"
-#include "storage/types.hpp"
-#include "transactions/commit_log.hpp"
-#include "transactions/snapshot.hpp"
-#include "transactions/type.hpp"
-
-namespace storage {
-
-#define ID_VALUE_RPC(type)                                           \
-  RPC_SINGLE_MEMBER_MESSAGE(type##IdReq, std::string);               \
-  RPC_SINGLE_MEMBER_MESSAGE(type##IdRes, storage::type);             \
-  using type##IdRpc =                                                \
-      communication::rpc::RequestResponse<type##IdReq, type##IdRes>; \
-  RPC_SINGLE_MEMBER_MESSAGE(Id##type##Req, storage::type);           \
-  RPC_SINGLE_MEMBER_MESSAGE(Id##type##Res, std::string);             \
-  using Id##type##Rpc =                                              \
-      communication::rpc::RequestResponse<Id##type##Req, Id##type##Res>;
-
-ID_VALUE_RPC(Label)
-ID_VALUE_RPC(EdgeType)
-ID_VALUE_RPC(Property)
-
-#undef ID_VALUE_RPC
-
-}  // namespace storage
diff --git a/src/storage/concurrent_id_mapper_rpc_messages.lcp b/src/storage/concurrent_id_mapper_rpc_messages.lcp
new file mode 100644
index 000000000..439cf3d8d
--- /dev/null
+++ b/src/storage/concurrent_id_mapper_rpc_messages.lcp
@@ -0,0 +1,44 @@
+#>cpp
+#pragma once
+
+#include <chrono>
+
+#include "communication/rpc/messages.hpp"
+#include "storage/concurrent_id_mapper_rpc_messages.capnp.h"
+#include "storage/types.hpp"
+#include "transactions/commit_log.hpp"
+#include "transactions/snapshot.hpp"
+#include "transactions/type.hpp"
+cpp<#
+
+(lcp:namespace storage)
+
+(lcp:capnp-namespace "storage")
+
+(lcp:capnp-import 's "/storage/serialization.capnp")
+
+(lcp:define-rpc label-id
+    (:request ((member "std::string")))
+  (:response ((member "Label" :capnp-type "S.Common"))))
+
+(lcp:define-rpc id-label
+    (:request ((member "Label" :capnp-type "S.Common")))
+  (:response ((member "std::string"))))
+
+(lcp:define-rpc edge-type-id
+    (:request ((member "std::string")))
+  (:response ((member "EdgeType" :capnp-type "S.Common"))))
+
+(lcp:define-rpc id-edge-type
+    (:request ((member "EdgeType" :capnp-type "S.Common")))
+  (:response ((member "std::string"))))
+
+(lcp:define-rpc property-id
+    (:request ((member "std::string")))
+  (:response ((member "Property" :capnp-type "S.Common"))))
+
+(lcp:define-rpc id-property
+    (:request ((member "Property" :capnp-type "S.Common")))
+  (:response ((member "std::string"))))
+
+(lcp:pop-namespace)
diff --git a/src/storage/types.capnp b/src/storage/serialization.capnp
similarity index 86%
rename from src/storage/types.capnp
rename to src/storage/serialization.capnp
index 08b81673c..8140fe015 100644
--- a/src/storage/types.capnp
+++ b/src/storage/serialization.capnp
@@ -15,3 +15,7 @@ struct Common {
 struct Label {}
 struct EdgeType {}
 struct Property {}
+
+struct Address {
+  storage @0 :UInt64;
+}
diff --git a/src/storage/types.hpp b/src/storage/types.hpp
index 6121a43ce..b7801fc00 100644
--- a/src/storage/types.hpp
+++ b/src/storage/types.hpp
@@ -6,8 +6,8 @@
 
 #include "boost/serialization/base_object.hpp"
 #include "glog/logging.h"
-#include "types.capnp.h"
 
+#include "storage/serialization.capnp.h"
 #include "utils/total_ordering.hpp"
 
 namespace storage {
diff --git a/src/transactions/commit_log.hpp b/src/transactions/commit_log.hpp
index e446f59a0..6081cc2f7 100644
--- a/src/transactions/commit_log.hpp
+++ b/src/transactions/commit_log.hpp
@@ -1,9 +1,8 @@
 #pragma once
 
-#include "boost/serialization/access.hpp"
-
 #include "data_structures/bitset/dynamic_bitset.hpp"
-#include "type.hpp"
+#include "transactions/common.capnp.h"
+#include "transactions/type.hpp"
 
 namespace tx {
 
@@ -57,14 +56,15 @@ class CommitLog {
 
     operator uint8_t() const { return flags_; }
 
-   private:
-    friend class boost::serialization::access;
-
-    template <class TArchive>
-    void serialize(TArchive &ar, unsigned int) {
-      ar &flags_;
+    void Save(capnp::CommitLogInfo::Builder *builder) const {
+      builder->setFlags(flags_);
     }
 
+    void Load(const capnp::CommitLogInfo::Reader &reader) {
+      flags_ = reader.getFlags();
+    }
+
+   private:
     uint8_t flags_{0};
   };
 
diff --git a/src/transactions/common.capnp b/src/transactions/common.capnp
new file mode 100644
index 000000000..f9999efe4
--- /dev/null
+++ b/src/transactions/common.capnp
@@ -0,0 +1,12 @@
+@0xcdbe169866471033;
+
+using Cxx = import "/capnp/c++.capnp";
+$Cxx.namespace("tx::capnp");
+
+struct Snapshot {
+  transactionIds @0 :List(UInt64);
+}
+
+struct CommitLogInfo {
+  flags @0 :UInt8;
+}
diff --git a/src/transactions/engine.hpp b/src/transactions/engine.hpp
index 04cb93ba3..00888a154 100644
--- a/src/transactions/engine.hpp
+++ b/src/transactions/engine.hpp
@@ -6,6 +6,7 @@
 
 #include "data_structures/concurrent/concurrent_map.hpp"
 #include "transactions/commit_log.hpp"
+#include "transactions/snapshot.hpp"
 #include "transactions/transaction.hpp"
 #include "transactions/type.hpp"
 
diff --git a/src/transactions/engine_master.cpp b/src/transactions/engine_master.cpp
index 5f984b486..6a081b893 100644
--- a/src/transactions/engine_master.cpp
+++ b/src/transactions/engine_master.cpp
@@ -15,61 +15,74 @@ MasterEngine::MasterEngine(communication::rpc::Server &server,
     : SingleNodeEngine(wal),
       rpc_server_(server),
       ongoing_produce_joiner_(rpc_worker_clients) {
-  rpc_server_.Register<BeginRpc>([this](const BeginReq &) {
-    auto tx = Begin();
-    return std::make_unique<BeginRes>(TxAndSnapshot{tx->id_, tx->snapshot()});
-  });
-
-  rpc_server_.Register<AdvanceRpc>([this](const AdvanceReq &req) {
-    return std::make_unique<AdvanceRes>(Advance(req.member));
-  });
-
-  rpc_server_.Register<CommitRpc>([this](const CommitReq &req) {
-    Commit(*RunningTransaction(req.member));
-    return std::make_unique<CommitRes>();
-  });
-
-  rpc_server_.Register<AbortRpc>([this](const AbortReq &req) {
-    Abort(*RunningTransaction(req.member));
-    return std::make_unique<AbortRes>();
-  });
-
-  rpc_server_.Register<SnapshotRpc>([this](const SnapshotReq &req) {
-    // It is guaranteed that the Worker will not be requesting this for a
-    // transaction that's done, and that there are no race conditions here.
-    return std::make_unique<SnapshotRes>(
-        RunningTransaction(req.member)->snapshot());
-  });
-
-  rpc_server_.Register<CommandRpc>([this](const CommandReq &req) {
-    // It is guaranteed that the Worker will not be requesting this for a
-    // transaction that's done, and that there are no race conditions here.
-    return std::make_unique<CommandRes>(RunningTransaction(req.member)->cid());
-  });
-
-  rpc_server_.Register<GcSnapshotRpc>(
-      [this](const communication::rpc::Message &) {
-        return std::make_unique<SnapshotRes>(GlobalGcSnapshot());
+  rpc_server_.Register<BeginRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        auto tx = this->Begin();
+        BeginRes res(TxAndSnapshot{tx->id_, tx->snapshot()});
+        res.Save(res_builder);
       });
 
-  rpc_server_.Register<ClogInfoRpc>([this](const ClogInfoReq &req) {
-    return std::make_unique<ClogInfoRes>(Info(req.member));
-  });
+  rpc_server_.Register<AdvanceRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        AdvanceRes res(this->Advance(req_reader.getMember()));
+        res.Save(res_builder);
+      });
+
+  rpc_server_.Register<CommitRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        this->Commit(*this->RunningTransaction(req_reader.getMember()));
+      });
+
+  rpc_server_.Register<AbortRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        this->Abort(*this->RunningTransaction(req_reader.getMember()));
+      });
+
+  rpc_server_.Register<SnapshotRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        // It is guaranteed that the Worker will not be requesting this for a
+        // transaction that's done, and that there are no race conditions here.
+        SnapshotRes res(
+            this->RunningTransaction(req_reader.getMember())->snapshot());
+        res.Save(res_builder);
+      });
+
+  rpc_server_.Register<CommandRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        // It is guaranteed that the Worker will not be requesting this for a
+        // transaction that's done, and that there are no race conditions here.
+        CommandRes res(this->RunningTransaction(req_reader.getMember())->cid());
+        res.Save(res_builder);
+      });
+
+  rpc_server_.Register<GcSnapshotRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        GcSnapshotRes res(this->GlobalGcSnapshot());
+        res.Save(res_builder);
+      });
+
+  rpc_server_.Register<ClogInfoRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        ClogInfoRes res(this->Info(req_reader.getMember()));
+        res.Save(res_builder);
+      });
 
   rpc_server_.Register<ActiveTransactionsRpc>(
-      [this](const communication::rpc::Message &) {
-        return std::make_unique<SnapshotRes>(GlobalActiveTransactions());
+      [this](const auto &req_reader, auto *res_builder) {
+        ActiveTransactionsRes res(this->GlobalActiveTransactions());
+        res.Save(res_builder);
       });
 
   rpc_server_.Register<EnsureNextIdGreaterRpc>(
-      [this](const EnsureNextIdGreaterReq &req) {
-        EnsureNextIdGreater(req.member);
-        return std::make_unique<EnsureNextIdGreaterRes>();
+      [this](const auto &req_reader, auto *res_builder) {
+        this->EnsureNextIdGreater(req_reader.getMember());
       });
 
-  rpc_server_.Register<GlobalLastRpc>([this](const GlobalLastReq &) {
-    return std::make_unique<GlobalLastRes>(GlobalLast());
-  });
+  rpc_server_.Register<GlobalLastRpc>(
+      [this](const auto &req_reader, auto *res_builder) {
+        GlobalLastRes res(this->GlobalLast());
+        res.Save(res_builder);
+      });
 }
 
 void MasterEngine::Commit(const Transaction &t) {
diff --git a/src/transactions/engine_rpc_messages.hpp b/src/transactions/engine_rpc_messages.hpp
deleted file mode 100644
index 9f948813c..000000000
--- a/src/transactions/engine_rpc_messages.hpp
+++ /dev/null
@@ -1,70 +0,0 @@
-#pragma once
-
-#include "communication/rpc/messages.hpp"
-#include "transactions/commit_log.hpp"
-#include "transactions/snapshot.hpp"
-#include "transactions/type.hpp"
-
-namespace tx {
-
-RPC_NO_MEMBER_MESSAGE(BeginReq);
-struct TxAndSnapshot {
-  TransactionId tx_id;
-  Snapshot snapshot;
-
- private:
-  friend class boost::serialization::access;
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &tx_id;
-    ar &snapshot;
-  }
-};
-RPC_SINGLE_MEMBER_MESSAGE(BeginRes, TxAndSnapshot);
-using BeginRpc = communication::rpc::RequestResponse<BeginReq, BeginRes>;
-
-RPC_SINGLE_MEMBER_MESSAGE(AdvanceReq, TransactionId);
-RPC_SINGLE_MEMBER_MESSAGE(AdvanceRes, CommandId);
-using AdvanceRpc = communication::rpc::RequestResponse<AdvanceReq, AdvanceRes>;
-
-RPC_SINGLE_MEMBER_MESSAGE(CommitReq, TransactionId);
-RPC_NO_MEMBER_MESSAGE(CommitRes);
-using CommitRpc = communication::rpc::RequestResponse<CommitReq, CommitRes>;
-
-RPC_SINGLE_MEMBER_MESSAGE(AbortReq, TransactionId);
-RPC_NO_MEMBER_MESSAGE(AbortRes);
-using AbortRpc = communication::rpc::RequestResponse<AbortReq, AbortRes>;
-
-RPC_SINGLE_MEMBER_MESSAGE(SnapshotReq, TransactionId);
-RPC_SINGLE_MEMBER_MESSAGE(SnapshotRes, Snapshot);
-using SnapshotRpc =
-    communication::rpc::RequestResponse<SnapshotReq, SnapshotRes>;
-
-RPC_SINGLE_MEMBER_MESSAGE(CommandReq, TransactionId);
-RPC_SINGLE_MEMBER_MESSAGE(CommandRes, CommandId);
-using CommandRpc = communication::rpc::RequestResponse<CommandReq, CommandRes>;
-
-RPC_NO_MEMBER_MESSAGE(GcSnapshotReq);
-using GcSnapshotRpc =
-    communication::rpc::RequestResponse<GcSnapshotReq, SnapshotRes>;
-
-RPC_SINGLE_MEMBER_MESSAGE(ClogInfoReq, TransactionId);
-RPC_SINGLE_MEMBER_MESSAGE(ClogInfoRes, CommitLog::Info);
-using ClogInfoRpc =
-    communication::rpc::RequestResponse<ClogInfoReq, ClogInfoRes>;
-
-RPC_NO_MEMBER_MESSAGE(ActiveTransactionsReq);
-using ActiveTransactionsRpc =
-    communication::rpc::RequestResponse<ActiveTransactionsReq, SnapshotRes>;
-
-RPC_SINGLE_MEMBER_MESSAGE(EnsureNextIdGreaterReq, TransactionId);
-RPC_NO_MEMBER_MESSAGE(EnsureNextIdGreaterRes);
-using EnsureNextIdGreaterRpc =
-    communication::rpc::RequestResponse<EnsureNextIdGreaterReq,
-                                        EnsureNextIdGreaterRes>;
-
-RPC_NO_MEMBER_MESSAGE(GlobalLastReq);
-RPC_SINGLE_MEMBER_MESSAGE(GlobalLastRes, TransactionId);
-using GlobalLastRpc =
-    communication::rpc::RequestResponse<GlobalLastReq, GlobalLastRes>;
-}  // namespace tx
diff --git a/src/transactions/engine_rpc_messages.lcp b/src/transactions/engine_rpc_messages.lcp
new file mode 100644
index 000000000..839ad6c57
--- /dev/null
+++ b/src/transactions/engine_rpc_messages.lcp
@@ -0,0 +1,69 @@
+#>cpp
+#pragma once
+
+#include "communication/rpc/messages.hpp"
+#include "transactions/commit_log.hpp"
+#include "transactions/engine_rpc_messages.capnp.h"
+#include "transactions/snapshot.hpp"
+#include "transactions/type.hpp"
+cpp<#
+
+(lcp:namespace tx)
+
+(lcp:capnp-namespace "tx")
+
+(lcp:capnp-import 'tx "/transactions/common.capnp")
+(lcp:capnp-type-conversion "TransactionId" "UInt64")
+(lcp:capnp-type-conversion "CommandId" "UInt32")
+(lcp:capnp-type-conversion "Snapshot" "Tx.Snapshot")
+
+(lcp:define-struct tx-and-snapshot ()
+  ((tx-id "TransactionId")
+   (snapshot "Snapshot"))
+  (:serialize :capnp))
+
+(lcp:define-rpc begin
+    (:request ())
+  (:response ((member "TxAndSnapshot"))))
+
+(lcp:define-rpc advance
+    (:request ((member "TransactionId")))
+  (:response ((member "CommandId"))))
+
+(lcp:define-rpc commit
+    (:request ((member "TransactionId")))
+  (:response ()))
+
+(lcp:define-rpc abort
+    (:request ((member "TransactionId")))
+  (:response ()))
+
+(lcp:define-rpc snapshot
+    (:request ((member "TransactionId")))
+  (:response ((member "Snapshot"))))
+
+(lcp:define-rpc command
+    (:request ((member "TransactionId")))
+  (:response ((member "CommandId"))))
+
+(lcp:define-rpc gc-snapshot
+    (:request ())
+  (:response ((member "Snapshot"))))
+
+(lcp:define-rpc clog-info
+    (:request ((member "TransactionId")))
+  (:response ((member "CommitLog::Info" :capnp-type "Tx.CommitLogInfo"))))
+
+(lcp:define-rpc active-transactions
+    (:request ())
+  (:response ((member "Snapshot"))))
+
+(lcp:define-rpc ensure-next-id-greater
+    (:request ((member "TransactionId")))
+  (:response ()))
+
+(lcp:define-rpc global-last
+    (:request ())
+  (:response ((member "TransactionId"))))
+
+(lcp:pop-namespace) ;; tx
diff --git a/src/transactions/snapshot.cpp b/src/transactions/snapshot.cpp
new file mode 100644
index 000000000..134259566
--- /dev/null
+++ b/src/transactions/snapshot.cpp
@@ -0,0 +1,16 @@
+#include "transactions/snapshot.hpp"
+
+#include "utils/serialization.hpp"
+
+namespace tx {
+
+void Snapshot::Save(capnp::Snapshot::Builder *builder) const {
+  auto list_builder = builder->initTransactionIds(transaction_ids_.size());
+  utils::SaveVector(transaction_ids_, &list_builder);
+}
+
+void Snapshot::Load(const capnp::Snapshot::Reader &reader) {
+  utils::LoadVector(&transaction_ids_, reader.getTransactionIds());
+}
+
+}  // namespace tx
diff --git a/src/transactions/snapshot.hpp b/src/transactions/snapshot.hpp
index 3cc1ca0d6..bb2549282 100644
--- a/src/transactions/snapshot.hpp
+++ b/src/transactions/snapshot.hpp
@@ -4,10 +4,8 @@
 #include <iostream>
 #include <vector>
 
-#include "boost/serialization/access.hpp"
-#include "boost/serialization/vector.hpp"
-
 #include "glog/logging.h"
+#include "transactions/common.capnp.h"
 #include "transactions/type.hpp"
 #include "utils/algorithm.hpp"
 
@@ -86,14 +84,11 @@ class Snapshot {
     return stream;
   }
 
+  void Save(capnp::Snapshot::Builder *builder) const;
+  void Load(const capnp::Snapshot::Reader &reader);
+
  private:
-  friend class boost::serialization::access;
-
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &transaction_ids_;
-  }
-
   std::vector<TransactionId> transaction_ids_;
 };
+
 }  // namespace tx
diff --git a/src/utils/serialization.capnp b/src/utils/serialization.capnp
index 8dfb73cd2..a25111477 100644
--- a/src/utils/serialization.capnp
+++ b/src/utils/serialization.capnp
@@ -1,10 +1,15 @@
 @0xe7647d63b36c2c65;
 
 using Cxx = import "/capnp/c++.capnp";
+
 $Cxx.namespace("utils::capnp");
 
+# Primitive type wrappers
+
+struct BoxInt16 {
+  value @0 :Int16;
+}
 
-# Primitive types wrappers.
 struct BoxInt32 {
   value @0 :Int32;
 }
@@ -13,12 +18,16 @@ struct BoxInt64 {
   value @0 :Int64;
 }
 
+struct BoxUInt16 {
+  value @0 :UInt16;
+}
+
 struct BoxUInt32 {
   value @0 :UInt32;
 }
 
 struct BoxUInt64 {
-  value @0 :UInt32;
+  value @0 :UInt64;
 }
 
 struct BoxFloat32 {
@@ -33,8 +42,7 @@ struct BoxBool {
   value @0 :Bool;
 }
 
-
-# CPP Types.
+# C++ STL types
 
 struct Optional(T) {
   union {
@@ -62,28 +70,22 @@ struct SharedPtr(T) {
   }
 }
 
-# Our types
-
-struct TypedValue {
-  union {
-    nullType @0 :Void;
-    bool @1 :Bool;
-    integer @2 :Int64;
-    double @3 :Float64;
-    string @4 :Text;
-    list @5 :List(TypedValue);
-    map @6 :List(Entry);
-    # TODO vertex accessor
-    # TODO edge accessor
-    # TODO path
-  }
+struct Map(K, V) {
+  entries @0 :List(Entry);
 
   struct Entry {
-    key @0 :Text;
-    value @1 :TypedValue;
+    key @0 :K;
+    value @1 :V;
   }
 }
 
+struct Pair(First, Second) {
+  first @0 :First;
+  second @1 :Second;
+}
+
+# Our types
+
 struct Bound(T) {
   type @0 :Type;
   value @1 :T;
diff --git a/src/utils/serialization.hpp b/src/utils/serialization.hpp
index fb8d51e02..aead65a8b 100644
--- a/src/utils/serialization.hpp
+++ b/src/utils/serialization.hpp
@@ -2,12 +2,15 @@
 
 #include <experimental/optional>
 
+#include "boost/serialization/optional.hpp"
+#include "boost/serialization/serialization.hpp"
 #include "boost/serialization/split_free.hpp"
+
+#include "distributed/serialization.capnp.h"
 #include "query/typed_value.hpp"
 #include "storage/edge.hpp"
 #include "storage/vertex.hpp"
 #include "utils/exceptions.hpp"
-
 #include "utils/serialization.capnp.h"
 
 namespace boost::serialization {
@@ -63,64 +66,114 @@ void load(TArchive &ar, std::experimental::optional<T> &opt,
 
 namespace utils {
 
-inline void SaveCapnpTypedValue(const query::TypedValue &value,
-                                capnp::TypedValue::Builder &builder) {
+inline void SaveCapnpTypedValue(
+    const query::TypedValue &value,
+    distributed::capnp::TypedValue::Builder *builder,
+    std::function<void(const query::TypedValue &,
+                       distributed::capnp::TypedValue::Builder *)>
+        save_graph_element = nullptr) {
   switch (value.type()) {
     case query::TypedValue::Type::Null:
-      builder.setNullType();
+      builder->setNullType();
       return;
     case query::TypedValue::Type::Bool:
-      builder.setBool(value.Value<bool>());
+      builder->setBool(value.Value<bool>());
       return;
     case query::TypedValue::Type::Int:
-      builder.setInteger(value.Value<int64_t>());
+      builder->setInteger(value.Value<int64_t>());
       return;
     case query::TypedValue::Type::Double:
-      builder.setDouble(value.Value<double>());
+      builder->setDouble(value.Value<double>());
       return;
     case query::TypedValue::Type::String:
-      builder.setString(value.Value<std::string>());
+      builder->setString(value.Value<std::string>());
       return;
-    case query::TypedValue::Type::List:
-    case query::TypedValue::Type::Map:
+    case query::TypedValue::Type::List: {
+      const auto &values = value.Value<std::vector<query::TypedValue>>();
+      auto list_builder = builder->initList(values.size());
+      for (size_t i = 0; i < values.size(); ++i) {
+        auto value_builder = list_builder[i];
+        SaveCapnpTypedValue(values[i], &value_builder, save_graph_element);
+      }
+      return;
+    }
+    case query::TypedValue::Type::Map: {
+      const auto &map = value.Value<std::map<std::string, query::TypedValue>>();
+      auto map_builder = builder->initMap(map.size());
+      size_t i = 0;
+      for (const auto &kv : map) {
+        auto kv_builder = map_builder[i];
+        kv_builder.setKey(kv.first);
+        auto value_builder = kv_builder.initValue();
+        SaveCapnpTypedValue(kv.second, &value_builder, save_graph_element);
+        ++i;
+      }
+      return;
+    }
     case query::TypedValue::Type::Vertex:
     case query::TypedValue::Type::Edge:
     case query::TypedValue::Type::Path:
-      throw utils::NotYetImplemented("Capnp serialize typed value");
+      if (save_graph_element) {
+        save_graph_element(value, builder);
+      } else {
+        throw utils::BasicException(
+            "Unable to serialize TypedValue of type: {}", value.type());
+      }
   }
 }
 
-inline void LoadCapnpTypedValue(query::TypedValue &value,
-                                capnp::TypedValue::Reader &reader) {
+inline void LoadCapnpTypedValue(
+    const distributed::capnp::TypedValue::Reader &reader,
+    query::TypedValue *value,
+    std::function<void(const distributed::capnp::TypedValue::Reader &,
+                       query::TypedValue *)>
+        load_graph_element = nullptr) {
   switch (reader.which()) {
-    case capnp::TypedValue::BOOL:
-      value = reader.getBool();
+    case distributed::capnp::TypedValue::NULL_TYPE:
+      *value = query::TypedValue::Null;
       return;
-    case capnp::TypedValue::DOUBLE:
-      value = reader.getDouble();
+    case distributed::capnp::TypedValue::BOOL:
+      *value = reader.getBool();
       return;
-    // case capnp::TypedValue::EDGE:
-    //   // TODO
-    //   return;
-    case capnp::TypedValue::INTEGER:
-      value = reader.getInteger();
+    case distributed::capnp::TypedValue::INTEGER:
+      *value = reader.getInteger();
       return;
-    case capnp::TypedValue::LIST:
-      throw utils::NotYetImplemented("Capnp deserialize typed value");
-    case capnp::TypedValue::MAP:
-      throw utils::NotYetImplemented("Capnp deserialize typed value");
-    case capnp::TypedValue::NULL_TYPE:
-      value = query::TypedValue::Null;
+    case distributed::capnp::TypedValue::DOUBLE:
+      *value = reader.getDouble();
       return;
-    // case query::capnp::TypedValue::PATH:
-    //   // TODO
-    //   return;
-    case capnp::TypedValue::STRING:
-      value = reader.getString().cStr();
+    case distributed::capnp::TypedValue::STRING:
+      *value = reader.getString().cStr();
       return;
-      // case query::capnp::TypedValue::VERTEX:
-      //   // TODO
-      //   return;
+    case distributed::capnp::TypedValue::LIST: {
+      std::vector<query::TypedValue> list;
+      list.reserve(reader.getList().size());
+      for (const auto &value_reader : reader.getList()) {
+        list.emplace_back();
+        LoadCapnpTypedValue(value_reader, &list.back(), load_graph_element);
+      }
+      *value = list;
+      return;
+    }
+    case distributed::capnp::TypedValue::MAP: {
+      std::map<std::string, query::TypedValue> map;
+      for (const auto &kv_reader : reader.getMap()) {
+        auto key = kv_reader.getKey().cStr();
+        LoadCapnpTypedValue(kv_reader.getValue(), &map[key],
+                            load_graph_element);
+      }
+      *value = map;
+      return;
+    }
+    case distributed::capnp::TypedValue::VERTEX:
+    case distributed::capnp::TypedValue::EDGE:
+    case distributed::capnp::TypedValue::PATH:
+      if (load_graph_element) {
+        load_graph_element(reader, value);
+      } else {
+        throw utils::BasicException(
+            "Unexpected TypedValue type '{}' when loading from archive",
+            reader.which());
+      }
   }
 }
 
@@ -161,6 +214,34 @@ inline void LoadVector(
   }
 }
 
+template <class TCapnpKey, class TCapnpValue, class TMap>
+void SaveMap(const TMap &map,
+             typename capnp::Map<TCapnpKey, TCapnpValue>::Builder *map_builder,
+             std::function<void(
+                 typename capnp::Map<TCapnpKey, TCapnpValue>::Entry::Builder *,
+                 const typename TMap::value_type &)>
+                 save) {
+  auto entries_builder = map_builder->initEntries(map.size());
+  size_t i = 0;
+  for (const auto &entry : map) {
+    auto entry_builder = entries_builder[i];
+    save(&entry_builder, entry);
+    ++i;
+  }
+}
+
+template <class TCapnpKey, class TCapnpValue, class TMap>
+void LoadMap(
+    TMap *map,
+    const typename capnp::Map<TCapnpKey, TCapnpValue>::Reader &map_reader,
+    std::function<typename TMap::value_type(
+        const typename capnp::Map<TCapnpKey, TCapnpValue>::Entry::Reader &)>
+        load) {
+  for (const auto &entry_reader : map_reader.getEntries()) {
+    map->insert(load(entry_reader));
+  }
+}
+
 template <typename TCapnp, typename T>
 inline void SaveOptional(
     const std::experimental::optional<T> &data,
diff --git a/src/utils/typed_value.capnp b/src/utils/typed_value.capnp
deleted file mode 100644
index cc977cd48..000000000
--- a/src/utils/typed_value.capnp
+++ /dev/null
@@ -1,25 +0,0 @@
-@0xd229a9c0f7e55750;
-
-using Cxx = import "/capnp/c++.capnp";
-$Cxx.namespace("query::capnp");
-
-
-struct TypedValue {
-  union {
-    nullType @0 :Void;
-    bool @1 :Bool;
-    integer @2 :Int64;
-    double @3 :Float64;
-    string @4 :Text;
-    list @5 :List(TypedValue);
-    map @6 :List(Entry);
-    # TODO vertex accessor
-    # TODO edge accessor
-    # TODO path
-  }
-
-  struct Entry {
-    key @0 :Text;
-    value @1 :TypedValue;
-  }
-}
diff --git a/tests/distributed/raft/example_client.cpp b/tests/distributed/raft/example_client.cpp
index b9afeeb5e..a6f547918 100644
--- a/tests/distributed/raft/example_client.cpp
+++ b/tests/distributed/raft/example_client.cpp
@@ -34,14 +34,15 @@ int main(int argc, char **argv) {
   // in correct order.
   for (int i = 1; i <= 100; ++i) {
     LOG(INFO) << fmt::format("Apennding value: {}", i);
-    auto result_tuple = client.Call<AppendEntry>(i);
-    if (!result_tuple) {
-      LOG(INFO) << "Request unsuccessful";
-      // Try to resend value
-      --i;
-    } else {
-      LOG(INFO) << fmt::format("Appended value: {}", i);
-    }
+    // TODO: Serialize RPC via Cap'n Proto
+    // auto result_tuple = client.Call<AppendEntry>(i);
+    // if (!result_tuple) {
+    //   LOG(INFO) << "Request unsuccessful";
+    //   // Try to resend value
+    //   --i;
+    // } else {
+    //   LOG(INFO) << fmt::format("Appended value: {}", i);
+    // }
   }
 
   return 0;
diff --git a/tests/distributed/raft/example_server.cpp b/tests/distributed/raft/example_server.cpp
index 72c4b18c6..1ec3cd00f 100644
--- a/tests/distributed/raft/example_server.cpp
+++ b/tests/distributed/raft/example_server.cpp
@@ -56,13 +56,17 @@ int main(int argc, char **argv) {
       << "Unable to register SIGINT handler!";
 
   // Example callback.
-  server.Register<AppendEntry>([&log](const AppendEntryReq &request) {
-    log << request.val << std::endl;
-    log.flush();
-    LOG(INFO) << fmt::format("AppendEntry: {}", request.val);
-    return std::make_unique<AppendEntryRes>(200, FLAGS_interface,
-                                            stol(FLAGS_port));
-  });
+  // TODO: Serialize RPC via Cap'n Proto
+  // server.Register<AppendEntry>(
+  //     [&log](const auto &req_reader, auto *res_builder) {
+  //       AppendEntryReq request;
+  //       request.Load(req_reader);
+  //       log << request.val << std::endl;
+  //       log.flush();
+  //       LOG(INFO) << fmt::format("AppendEntry: {}", request.val);
+  //       AppendEntryRes res(200, FLAGS_interface, stol(FLAGS_port));
+  //       res.Save(res_builder);
+  //     });
 
   LOG(INFO) << "Raft RPC server started";
   // Sleep until shutdown detected.
diff --git a/tests/distributed/raft/messages.hpp b/tests/distributed/raft/messages.hpp
index 5cb33f3f3..e79f5e68a 100644
--- a/tests/distributed/raft/messages.hpp
+++ b/tests/distributed/raft/messages.hpp
@@ -1,30 +1,14 @@
-#include "boost/archive/binary_iarchive.hpp"
-#include "boost/archive/binary_oarchive.hpp"
-#include "boost/serialization/base_object.hpp"
-
-#include "boost/serialization/export.hpp"
 #include "communication/rpc/messages.hpp"
 
-using boost::serialization::base_object;
-using communication::rpc::Message;
 using namespace communication::rpc;
 
-struct AppendEntryReq : public Message {
+struct AppendEntryReq {
   AppendEntryReq() {}
   explicit AppendEntryReq(int val) : val(val) {}
   int val;
-
- private:
-  friend class boost::serialization::access;
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &base_object<Message>(*this);
-    ar &val;
-  }
 };
-BOOST_CLASS_EXPORT(AppendEntryReq);
 
-struct AppendEntryRes : public Message {
+struct AppendEntryRes {
   AppendEntryRes() {}
   AppendEntryRes(int status, std::string interface, uint16_t port)
       : status(status), interface(interface), port(port) {}
@@ -32,16 +16,6 @@ struct AppendEntryRes : public Message {
   std::string interface;
   uint16_t port;
 
- private:
-  friend class boost::serialization::access;
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &base_object<Message>(*this);
-    ar &status;
-    ar &interface;
-    ar &port;
-  }
 };
-BOOST_CLASS_EXPORT(AppendEntryRes);
 
 using AppendEntry = RequestResponse<AppendEntryReq, AppendEntryRes>;
diff --git a/tests/macro_benchmark/clients/card_fraud_client.cpp b/tests/macro_benchmark/clients/card_fraud_client.cpp
index 3a70a64c9..08ba98cb3 100644
--- a/tests/macro_benchmark/clients/card_fraud_client.cpp
+++ b/tests/macro_benchmark/clients/card_fraud_client.cpp
@@ -11,15 +11,6 @@
 
 #include "long_running_common.hpp"
 
-// TODO(mtomic): this sucks but I don't know a different way to make it work
-#include "boost/archive/binary_iarchive.hpp"
-#include "boost/archive/binary_oarchive.hpp"
-#include "boost/serialization/export.hpp"
-BOOST_CLASS_EXPORT(stats::StatsReq);
-BOOST_CLASS_EXPORT(stats::StatsRes);
-BOOST_CLASS_EXPORT(stats::BatchStatsReq);
-BOOST_CLASS_EXPORT(stats::BatchStatsRes);
-
 std::atomic<int64_t> num_pos;
 std::atomic<int64_t> num_cards;
 std::atomic<int64_t> num_transactions;
diff --git a/tests/manual/raft_rpc.cpp b/tests/manual/raft_rpc.cpp
index c423acd8c..428b3bceb 100644
--- a/tests/manual/raft_rpc.cpp
+++ b/tests/manual/raft_rpc.cpp
@@ -1,11 +1,3 @@
-#include "boost/serialization/export.hpp"
-
-#include "boost/archive/binary_iarchive.hpp"
-#include "boost/archive/binary_oarchive.hpp"
-#include "boost/archive/text_iarchive.hpp"
-#include "boost/archive/text_oarchive.hpp"
-#include "boost/serialization/export.hpp"
-
 #include "communication/raft/rpc.hpp"
 #include "communication/raft/storage/file.hpp"
 #include "communication/raft/test_utils.hpp"
@@ -22,9 +14,6 @@ using raft::test_utils::DummyState;
 DEFINE_string(member_id, "", "id of Raft member");
 DEFINE_string(log_dir, "", "Raft log directory");
 
-BOOST_CLASS_EXPORT(raft::PeerRpcReply);
-BOOST_CLASS_EXPORT(raft::PeerRpcRequest<DummyState>);
-
 /* Start cluster members with:
  * ./raft_rpc --member-id a --log-dir a_log
  * ./raft_rpc --member-id b --log-dir b_log
@@ -43,18 +32,19 @@ int main(int argc, char *argv[]) {
       {"c", Endpoint("127.0.0.1", 12347)}};
 
   communication::rpc::Server server(directory[FLAGS_member_id]);
-  RpcNetwork<DummyState> network(server, directory);
-  raft::SimpleFileStorage<DummyState> storage(FLAGS_log_dir);
+  // TODO: Serialize RPC via Cap'n Proto
+  // RpcNetwork<DummyState> network(server, directory);
+  // raft::SimpleFileStorage<DummyState> storage(FLAGS_log_dir);
 
-  raft::RaftConfig config{{"a", "b", "c"}, 150ms, 300ms, 70ms, 30ms};
+  // raft::RaftConfig config{{"a", "b", "c"}, 150ms, 300ms, 70ms, 30ms};
 
-  {
-    raft::RaftMember<DummyState> raft_member(network, storage, FLAGS_member_id,
-                                             config);
-    while (true) {
-      continue;
-    }
-  }
+  // {
+  //   raft::RaftMember<DummyState> raft_member(network, storage, FLAGS_member_id,
+  //                                            config);
+  //   while (true) {
+  //     continue;
+  //   }
+  // }
 
   return 0;
 }
diff --git a/tests/unit/rpc.cpp b/tests/unit/rpc.cpp
index d22bbeee5..09de7a552 100644
--- a/tests/unit/rpc.cpp
+++ b/tests/unit/rpc.cpp
@@ -1,12 +1,6 @@
 #include <thread>
 
-#include "boost/archive/binary_iarchive.hpp"
-#include "boost/archive/binary_oarchive.hpp"
-#include "boost/archive/text_iarchive.hpp"
-#include "boost/archive/text_oarchive.hpp"
-#include "boost/serialization/access.hpp"
-#include "boost/serialization/base_object.hpp"
-#include "boost/serialization/export.hpp"
+#include "capnp/serialize.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 
@@ -19,63 +13,85 @@
 using namespace communication::rpc;
 using namespace std::literals::chrono_literals;
 
-struct SumReq : public Message {
+struct SumReq {
+  using Capnp = ::capnp::AnyPointer;
+  static const MessageType TypeInfo;
+
+  SumReq() {}  // Needed for serialization.
   SumReq(int x, int y) : x(x), y(y) {}
   int x;
   int y;
 
- private:
-  friend class boost::serialization::access;
-  SumReq() {}  // Needed for serialization.
+  void Save(::capnp::AnyPointer::Builder *builder) const {
+    auto list_builder = builder->initAs<::capnp::List<int>>(2);
+    list_builder.set(0, x);
+    list_builder.set(1, y);
+  }
 
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<Message>(*this);
-    ar &x;
-    ar &y;
+  void Load(const ::capnp::AnyPointer::Reader &reader) {
+    auto list_reader = reader.getAs<::capnp::List<int>>();
+    x = list_reader[0];
+    y = list_reader[1];
   }
 };
-BOOST_CLASS_EXPORT(SumReq);
 
-struct SumRes : public Message {
+const MessageType SumReq::TypeInfo{0, "SumReq"};
+
+struct SumRes {
+  using Capnp = ::capnp::AnyPointer;
+  static const MessageType TypeInfo;
+
+  SumRes() {}  // Needed for serialization.
   SumRes(int sum) : sum(sum) {}
+
   int sum;
 
- private:
-  friend class boost::serialization::access;
-  SumRes() {}  // Needed for serialization.
+  void Save(::capnp::AnyPointer::Builder *builder) const {
+    auto list_builder = builder->initAs<::capnp::List<int>>(1);
+    list_builder.set(0, sum);
+  }
 
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<Message>(*this);
-    ar &sum;
+  void Load(const ::capnp::AnyPointer::Reader &reader) {
+    auto list_reader = reader.getAs<::capnp::List<int>>();
+    sum = list_reader[0];
   }
 };
-BOOST_CLASS_EXPORT(SumRes);
+
+const MessageType SumRes::TypeInfo{1, "SumRes"};
+
 using Sum = RequestResponse<SumReq, SumRes>;
 
-struct EchoMessage : public Message {
+struct EchoMessage {
+  using Capnp = ::capnp::AnyPointer;
+  static const MessageType TypeInfo;
+
+  EchoMessage() {}  // Needed for serialization.
   EchoMessage(const std::string &data) : data(data) {}
+
   std::string data;
 
- private:
-  friend class boost::serialization::access;
-  EchoMessage() {}  // Needed for serialization.
+  void Save(::capnp::AnyPointer::Builder *builder) const {
+    auto list_builder = builder->initAs<::capnp::List<::capnp::Text>>(1);
+    list_builder.set(0, data);
+  }
 
-  template <class TArchive>
-  void serialize(TArchive &ar, unsigned int) {
-    ar &boost::serialization::base_object<Message>(*this);
-    ar &data;
+  void Load(const ::capnp::AnyPointer::Reader &reader) {
+    auto list_reader = reader.getAs<::capnp::List<::capnp::Text>>();
+    data = list_reader[0];
   }
 };
-BOOST_CLASS_EXPORT(EchoMessage);
+
+const MessageType EchoMessage::TypeInfo{2, "EchoMessage"};
 
 using Echo = RequestResponse<EchoMessage, EchoMessage>;
 
 TEST(Rpc, Call) {
   Server server({"127.0.0.1", 0});
-  server.Register<Sum>([](const SumReq &request) {
-    return std::make_unique<SumRes>(request.x + request.y);
+  server.Register<Sum>([](const auto &req_reader, auto *res_builder) {
+    SumReq req;
+    req.Load(req_reader);
+    SumRes res(req.x + req.y);
+    res.Save(res_builder);
   });
   std::this_thread::sleep_for(100ms);
 
@@ -87,9 +103,12 @@ TEST(Rpc, Call) {
 
 TEST(Rpc, Abort) {
   Server server({"127.0.0.1", 0});
-  server.Register<Sum>([](const SumReq &request) {
+  server.Register<Sum>([](const auto &req_reader, auto *res_builder) {
+    SumReq req;
+    req.Load(req_reader);
     std::this_thread::sleep_for(500ms);
-    return std::make_unique<SumRes>(request.x + request.y);
+    SumRes res(req.x + req.y);
+    res.Save(res_builder);
   });
   std::this_thread::sleep_for(100ms);
 
@@ -111,9 +130,12 @@ TEST(Rpc, Abort) {
 
 TEST(Rpc, ClientPool) {
   Server server({"127.0.0.1", 0});
-  server.Register<Sum>([](const SumReq &request) {
+  server.Register<Sum>([](const auto &req_reader, auto *res_builder) {
+    SumReq req;
+    req.Load(req_reader);
     std::this_thread::sleep_for(100ms);
-    return std::make_unique<SumRes>(request.x + request.y);
+    SumRes res(req.x + req.y);
+    res.Save(res_builder);
   });
   std::this_thread::sleep_for(100ms);
 
@@ -161,8 +183,10 @@ TEST(Rpc, ClientPool) {
 
 TEST(Rpc, LargeMessage) {
   Server server({"127.0.0.1", 0});
-  server.Register<Echo>([](const EchoMessage &request) {
-    return std::make_unique<EchoMessage>(request.data);
+  server.Register<Echo>([](const auto &req_reader, auto *res_builder) {
+    EchoMessage res;
+    res.Load(req_reader);
+    res.Save(res_builder);
   });
   std::this_thread::sleep_for(100ms);
 
diff --git a/tests/unit/rpc_worker_clients.cpp b/tests/unit/rpc_worker_clients.cpp
index 7ca8fa7da..368ecabc5 100644
--- a/tests/unit/rpc_worker_clients.cpp
+++ b/tests/unit/rpc_worker_clients.cpp
@@ -1,8 +1,6 @@
 #include <mutex>
 
-#include "boost/archive/binary_iarchive.hpp"
-#include "boost/archive/binary_oarchive.hpp"
-#include "boost/serialization/export.hpp"
+#include "capnp/serialize.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 
@@ -18,17 +16,35 @@
 
 namespace distributed {
 
-RPC_NO_MEMBER_MESSAGE(IncrementCounterReq);
-RPC_NO_MEMBER_MESSAGE(IncrementCounterRes);
+struct IncrementCounterReq {
+  using Capnp = ::capnp::AnyPointer;
+  static const communication::rpc::MessageType TypeInfo;
+
+  void Save(::capnp::AnyPointer::Builder *) const {}
+
+  void Load(const ::capnp::AnyPointer::Reader &) {}
+};
+
+const communication::rpc::MessageType IncrementCounterReq::TypeInfo{
+    0, "IncrementCounterReq"};
+
+struct IncrementCounterRes {
+  using Capnp = ::capnp::AnyPointer;
+  static const communication::rpc::MessageType TypeInfo;
+
+  void Save(::capnp::AnyPointer::Builder *) const {}
+
+  void Load(const ::capnp::AnyPointer::Reader &) {}
+};
+
+const communication::rpc::MessageType IncrementCounterRes::TypeInfo{
+    1, "IncrementCounterRes"};
 
 using IncrementCounterRpc =
     communication::rpc::RequestResponse<IncrementCounterReq,
                                         IncrementCounterRes>;
 };  // namespace distributed
 
-BOOST_CLASS_EXPORT(distributed::IncrementCounterReq);
-BOOST_CLASS_EXPORT(distributed::IncrementCounterRes);
-
 class RpcWorkerClientsTest : public ::testing::Test {
  protected:
   const io::network::Endpoint kLocalHost{"127.0.0.1", 0};
@@ -51,10 +67,9 @@ class RpcWorkerClientsTest : public ::testing::Test {
       cluster_discovery_.back()->RegisterWorker(i);
 
       workers_server_.back()->Register<distributed::IncrementCounterRpc>(
-          [this, i](const distributed::IncrementCounterReq &) {
+          [this, i](const auto &req_reader, auto *res_builder) {
             std::unique_lock<std::mutex> lock(mutex_);
             workers_cnt_[i]++;
-            return std::make_unique<distributed::IncrementCounterRes>();
           });
     }
   }
diff --git a/tests/unit/serialization.cpp b/tests/unit/serialization.cpp
index 83ab7f6e8..ac255efe9 100644
--- a/tests/unit/serialization.cpp
+++ b/tests/unit/serialization.cpp
@@ -361,3 +361,30 @@ TEST(Serialization, CapnpVectorNonCopyable) {
   EXPECT_EQ(*elements[0], 5);
   EXPECT_EQ(*elements[1], 10);
 }
+
+TEST(Serialization, CapnpMap) {
+  std::map<std::string, std::string> map{{"my_key", "my_value"},
+                                         {"other_key", "other_value"}};
+  ::capnp::MallocMessageBuilder message;
+  {
+    auto map_builder =
+        message.initRoot<utils::capnp::Map<capnp::Text, capnp::Text>>();
+    utils::SaveMap<capnp::Text, capnp::Text>(
+        map, &map_builder, [](auto *entry_builder, const auto &entry) {
+          entry_builder->setKey(entry.first);
+          entry_builder->setValue(entry.second);
+        });
+  }
+  std::map<std::string, std::string> new_map;
+  {
+    auto map_reader =
+        message.getRoot<utils::capnp::Map<capnp::Text, capnp::Text>>();
+    utils::LoadMap<capnp::Text, capnp::Text>(
+        &new_map, map_reader, [](const auto &entry_reader) {
+          std::string key = entry_reader.getKey();
+          std::string value = entry_reader.getValue();
+          return std::make_pair(key, value);
+        });
+  }
+  EXPECT_EQ(new_map, map);
+}
diff --git a/tools/lcp b/tools/lcp
index 38d40a07a..284798ab9 100755
--- a/tools/lcp
+++ b/tools/lcp
@@ -22,7 +22,7 @@ fi
 
 capnp=""
 if [[ $# -eq 2 ]]; then
-  capnp=":capnp-id \"$($2 id)\""
+  capnp=":capnp-id \"$2\""
 fi
 
 echo \
@@ -37,6 +37,6 @@ filename=`basename $lcp_file .lcp`
 hpp_file="$(dirname $lcp_file)/$filename.hpp"
 clang-format -style=file -i $hpp_file
 
-if [[ $# -eq 2 ]]; then
+if [[ $# -eq 2 && -w  "$lcp_file.cpp" ]]; then
   clang-format -style=file -i "$lcp_file.cpp"
 fi
diff --git a/tools/src/mg_statsd/main.cpp b/tools/src/mg_statsd/main.cpp
index 4bc68bceb..dbf337dfa 100644
--- a/tools/src/mg_statsd/main.cpp
+++ b/tools/src/mg_statsd/main.cpp
@@ -41,23 +41,31 @@ int main(int argc, char *argv[]) {
       << "Failed to connect to Graphite";
   graphite_socket.SetKeepAlive();
 
-  server.Register<stats::StatsRpc>([&](const stats::StatsReq &req) {
-    LOG(INFO) << "StatsRpc::Received";
-    std::string data = GraphiteFormat(req);
-    graphite_socket.Write(data);
-    return std::make_unique<stats::StatsRes>();
-  });
+  server.Register<stats::StatsRpc>(
+      [&](const auto &req_reader, auto *res_builder) {
+        stats::StatsReq req;
+        req.Load(req_reader);
+        LOG(INFO) << "StatsRpc::Received";
+        std::string data = GraphiteFormat(req);
+        graphite_socket.Write(data);
+        stats::StatsRes res;
+        res.Save(res_builder);
+      });
 
-  server.Register<stats::BatchStatsRpc>([&](const stats::BatchStatsReq &req) {
-    // TODO(mtomic): batching?
-    LOG(INFO) << fmt::format("BatchStatsRpc::Received: {}",
-                             req.requests.size());
-    for (size_t i = 0; i < req.requests.size(); ++i) {
-      std::string data = GraphiteFormat(req.requests[i]);
-      graphite_socket.Write(data, i + 1 < req.requests.size());
-    }
-    return std::make_unique<stats::BatchStatsRes>();
-  });
+  server.Register<stats::BatchStatsRpc>(
+      [&](const auto &req_reader, auto *res_builder) {
+        // TODO(mtomic): batching?
+        stats::BatchStatsReq req;
+        req.Load(req_reader);
+        LOG(INFO) << fmt::format("BatchStatsRpc::Received: {}",
+                                 req.requests.size());
+        for (size_t i = 0; i < req.requests.size(); ++i) {
+          std::string data = GraphiteFormat(req.requests[i]);
+          graphite_socket.Write(data, i + 1 < req.requests.size());
+        }
+        stats::BatchStatsRes res;
+        res.Save(res_builder);
+      });
 
   std::this_thread::sleep_until(std::chrono::system_clock::time_point::max());
 
diff --git a/tools/tests/statsd/mg_statsd_client.cpp b/tools/tests/statsd/mg_statsd_client.cpp
index 778417724..f96343236 100644
--- a/tools/tests/statsd/mg_statsd_client.cpp
+++ b/tools/tests/statsd/mg_statsd_client.cpp
@@ -7,13 +7,6 @@
 
 // TODO (buda): move this logic to a unit test
 
-// TODO (mtomic): This is a hack. I don't know a better way to make this work.
-#include "boost/archive/binary_iarchive.hpp"
-#include "boost/archive/binary_oarchive.hpp"
-#include "boost/serialization/export.hpp"
-BOOST_CLASS_EXPORT(stats::StatsReq);
-BOOST_CLASS_EXPORT(stats::StatsRes);
-
 bool parse_input(const std::string &s, std::string &metric_path,
                  std::vector<std::pair<std::string, std::string>> &tags,
                  double &value) {