You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by dc...@apache.org on 2022/10/21 20:14:48 UTC

[cassandra] branch cep-15-accord updated: CEP-15 (C*): Messaging and storage engine integration

This is an automated email from the ASF dual-hosted git repository.

dcapwell pushed a commit to branch cep-15-accord
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/cep-15-accord by this push:
     new e6cbeb39e0 CEP-15 (C*): Messaging and storage engine integration
e6cbeb39e0 is described below

commit e6cbeb39e0dbf4a20798abe828b744f84967cf1f
Author: Blake Eggleston <bl...@ultrablake.com>
AuthorDate: Fri Oct 21 13:10:08 2022 -0700

    CEP-15 (C*): Messaging and storage engine integration
    
    patch by Blake Eggleston; reviewed by Benedict Elliott Smith, David Capwell for CASSANDRA-17103
---
 .build/cassandra-deps-template.xml                 |   4 +
 .build/include-accord.sh                           |  58 ++
 .build/parent-pom-template.xml                     |   5 +
 .gitignore                                         |   1 +
 CHANGES.txt                                        |   3 +
 build.xml                                          |   7 +-
 simulator.sh                                       |  88 ++
 .../concurrent/SingleThreadExecutorPlus.java       |   1 +
 .../org/apache/cassandra/concurrent/Stage.java     |   2 +-
 src/java/org/apache/cassandra/config/Config.java   |   1 +
 .../cassandra/config/DatabaseDescriptor.java       |  17 +
 .../cql3/statements/ModificationStatement.java     |   2 +-
 .../apache/cassandra/db/MutableDeletionInfo.java   |  10 +
 .../apache/cassandra/db/RangeTombstoneList.java    |   9 +
 .../cassandra/db/SinglePartitionReadCommand.java   |  18 +
 .../db/filter/AbstractClusteringIndexFilter.java   |   1 +
 .../db/filter/ClusteringIndexNamesFilter.java      |  14 -
 .../db/filter/ClusteringIndexSliceFilter.java      |   1 +
 .../apache/cassandra/db/partitions/Partition.java  |   3 +
 .../cassandra/db/partitions/PartitionUpdate.java   |  17 +
 .../org/apache/cassandra/db/rows/AbstractCell.java |   7 +
 .../org/apache/cassandra/db/rows/BTreeRow.java     |  12 +
 .../org/apache/cassandra/db/rows/ColumnData.java   |   1 +
 .../cassandra/db/rows/ComplexColumnData.java       |   7 +
 src/java/org/apache/cassandra/db/rows/Row.java     |   2 +
 .../cassandra/dht/ByteOrderedPartitioner.java      |   6 +
 .../org/apache/cassandra/dht/LocalPartitioner.java |   6 +
 .../apache/cassandra/dht/Murmur3Partitioner.java   |   6 +
 .../cassandra/dht/OrderPreservingPartitioner.java  |   6 +
 .../apache/cassandra/dht/RandomPartitioner.java    |   6 +
 src/java/org/apache/cassandra/dht/Token.java       |   1 +
 src/java/org/apache/cassandra/metrics/Sampler.java |   1 +
 .../org/apache/cassandra/net/ForwardingInfo.java   |   1 +
 src/java/org/apache/cassandra/net/Message.java     |   3 +-
 .../apache/cassandra/net/ResponseVerbHandler.java  |   2 +-
 src/java/org/apache/cassandra/net/Verb.java        | 141 +++-
 src/java/org/apache/cassandra/schema/Schema.java   |   3 +-
 .../apache/cassandra/schema/SchemaConstants.java   |   3 +-
 src/java/org/apache/cassandra/schema/TableId.java  |  25 +-
 .../cassandra/service/accord/AccordCallback.java   |  69 ++
 .../cassandra/service/accord/AccordCommand.java    | 788 ++++++++++++++++++
 .../service/accord/AccordCommandStore.java         | 291 +++++++
 .../service/accord/AccordCommandStores.java        |  65 ++
 .../service/accord/AccordCommandsForKey.java       | 459 ++++++++++
 .../service/accord/AccordConfigurationService.java |  88 ++
 .../cassandra/service/accord/AccordKeyspace.java   | 843 +++++++++++++++++++
 .../service/accord/AccordMessageSink.java          | 120 +++
 .../service/accord/AccordObjectSizes.java          | 101 +++
 .../service/accord/AccordPartialCommand.java       | 338 ++++++++
 .../cassandra/service/accord/AccordService.java    | 187 +++++
 .../cassandra/service/accord/AccordState.java      | 113 +++
 .../cassandra/service/accord/AccordStateCache.java | 646 ++++++++++++++
 .../service/accord/AccordTopologyUtils.java        | 136 +++
 .../cassandra/service/accord/AccordTxnBuilder.java | 209 +++++
 .../service/accord/AccordVerbHandler.java          |  33 +-
 .../cassandra/service/accord/EndpointMapping.java  | 145 ++++
 .../cassandra/service/accord/ListenerProxy.java    | 279 +++++++
 .../cassandra/service/accord/ReadFuture.java       | 295 +++++++
 .../cassandra/service/accord/TokenRange.java       |  86 ++
 .../cassandra/service/accord/api/AccordAgent.java  |  31 +-
 .../cassandra/service/accord/api/AccordKey.java    | 480 +++++++++++
 .../service/accord/api/AccordScheduler.java        |  97 +++
 .../service/accord/async/AsyncContext.java         | 116 +++
 .../service/accord/async/AsyncLoader.java          | 240 ++++++
 .../service/accord/async/AsyncOperation.java       | 253 ++++++
 .../service/accord/async/AsyncWriter.java          | 321 +++++++
 .../service/accord/db/AbstractKeyIndexed.java      | 188 +++++
 .../cassandra/service/accord/db/AccordData.java    | 189 +++++
 .../cassandra/service/accord/db/AccordQuery.java   |  89 ++
 .../cassandra/service/accord/db/AccordRead.java    | 138 +++
 .../cassandra/service/accord/db/AccordUpdate.java  | 927 +++++++++++++++++++++
 .../cassandra/service/accord/db/AccordWrite.java   | 102 +++
 .../accord/serializers/AcceptSerializers.java      | 179 ++++
 .../serializers/ApplyAndCheckSerializers.java      |  99 +++
 .../accord/serializers/ApplySerializers.java       |  92 ++
 .../serializers/BeginInvalidateSerializers.java    | 126 +++
 .../accord/serializers/CheckStatusSerializers.java | 163 ++++
 .../accord/serializers/CommandSerializers.java     | 272 ++++++
 .../accord/serializers/CommitSerializers.java      |  93 +++
 .../InformOfPersistenceSerializers.java            |  63 ++
 .../accord/serializers/InformOfTxnSerializers.java |  82 ++
 .../service/accord/serializers/KeySerializers.java |  98 +++
 .../accord/serializers/NullableSerializer.java     |  48 ++
 .../accord/serializers/PreacceptSerializers.java   | 104 +++
 .../accord/serializers/ReadDataSerializers.java    | 108 +++
 .../accord/serializers/RecoverySerializers.java    | 192 +++++
 .../accord/serializers/TopologySerializers.java    |  73 ++
 .../accord/serializers/TxnRequestSerializer.java   | 100 +++
 .../accord/serializers/WaitOnCommitSerializer.java |  73 ++
 .../service/accord/store/AbstractStoredField.java  | 152 ++++
 .../service/accord/store/StoredBoolean.java        |  85 ++
 .../cassandra/service/accord/store/StoredLong.java |  86 ++
 .../service/accord/store/StoredNavigableMap.java   | 201 +++++
 .../cassandra/service/accord/store/StoredSet.java  | 243 ++++++
 .../service/accord/store/StoredValue.java          | 128 +++
 .../org/apache/cassandra/utils/ByteBufferUtil.java |   7 +
 src/java/org/apache/cassandra/utils/Hex.java       |  13 +-
 .../org/apache/cassandra/utils/btree/BTreeSet.java |   9 +
 .../cassandra/utils/concurrent/FutureCombiner.java |   1 +
 .../cassandra/utils/concurrent/Semaphore.java      |   1 +
 test/conf/logback-simulator.xml                    |  10 +-
 .../cassandra/distributed/impl/Instance.java       |   5 +-
 .../cassandra/distributed/impl/MessageImpl.java    |   1 +
 .../distributed/impl/UnsafeGossipHelper.java       |   1 +
 .../shared/VersionedApplicationState.java          |   1 +
 .../test/accord/AccordIntegrationTest.java         | 441 ++++++++++
 .../accord/AccordTopologyTest.java}                |  29 +-
 .../distributed/util/QueryResultUtil.java          |  31 +
 .../apache/cassandra/simulator/ActionSchedule.java |  12 +-
 .../cassandra/simulator/ClusterSimulation.java     |   3 +
 .../main/org/apache/cassandra/simulator/Debug.java |  32 +-
 .../cassandra/simulator/SimulationRunner.java      |  22 +-
 .../simulator/cluster/KeyspaceActions.java         |  12 +-
 .../simulator/logging/RunStartDefiner.java}        |  25 +-
 .../cassandra/simulator/logging/SeedDefiner.java}  |  27 +-
 ...=> AbstractPairOfSequencesPaxosSimulation.java} | 149 +---
 .../simulator/paxos/AccordClusterSimulation.java   |  87 ++
 .../simulator/paxos/AccordSimulationRunner.java    |  78 ++
 .../paxos/PairOfSequencesAccordSimulation.java     | 304 +++++++
 .../paxos/PairOfSequencesPaxosSimulation.java      | 222 +----
 .../cassandra/simulator/paxos/PaxosSimulation.java |  18 +-
 .../systems/InterceptingGlobalMethods.java         |   1 -
 .../simulator/systems/InterceptingMonitors.java    |  16 +-
 .../systems/InterceptorOfGlobalMethods.java        |  13 +-
 .../simulator/systems/SimulatedAction.java         |   3 +-
 .../simulator/test/ShortAccordSimulationTest.java} |  21 +-
 .../cql3/statements/DescribeStatementTest.java     |   1 +
 .../org/apache/cassandra/dht/KeyCollisionTest.java |   6 +
 .../service/accord/AccordCommandStoreTest.java     | 182 ++++
 .../service/accord/AccordCommandTest.java          | 186 +++++
 .../service/accord/AccordStateCacheTest.java       | 494 +++++++++++
 .../cassandra/service/accord/AccordTestUtils.java  | 257 ++++++
 .../service/accord/AccordTopologyTest.java         |  71 ++
 .../service/accord/EndpointMappingTest.java}       |  27 +-
 .../service/accord/api/AccordKeyTest.java          | 116 +++
 .../service/accord/async/AsyncLoaderTest.java      | 342 ++++++++
 .../service/accord/async/AsyncOperationTest.java   | 232 ++++++
 .../service/accord/async/AsyncWriterTest.java      | 252 ++++++
 .../service/accord/db/AccordUpdateTest.java        |  58 ++
 .../accord/serializers/CommandSerializersTest.java |  54 ++
 .../serializers/TopologySerializersTest.java}      |  25 +-
 .../service/accord/store/StoredMapTest.java        | 203 +++++
 .../service/accord/store/StoredSetTest.java        | 202 +++++
 .../service/accord/store/StoredValueTest.java      |  85 ++
 .../cassandra/utils/SerializerTestUtils.java       |  74 ++
 145 files changed, 15785 insertions(+), 500 deletions(-)

diff --git a/.build/cassandra-deps-template.xml b/.build/cassandra-deps-template.xml
index 513fcbc5aa..998720e7f2 100644
--- a/.build/cassandra-deps-template.xml
+++ b/.build/cassandra-deps-template.xml
@@ -129,6 +129,10 @@
       <groupId>org.mindrot</groupId>
       <artifactId>jbcrypt</artifactId>
     </dependency>
+    <dependency>
+      <groupId>accord</groupId>
+      <artifactId>accord</artifactId>
+    </dependency>
     <dependency>
       <groupId>io.airlift</groupId>
       <artifactId>airline</artifactId>
diff --git a/.build/include-accord.sh b/.build/include-accord.sh
new file mode 100755
index 0000000000..7069e22b6f
--- /dev/null
+++ b/.build/include-accord.sh
@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#set -o xtrace
+set -o errexit
+set -o pipefail
+set -o nounset
+
+bin="$(cd "$(dirname "$0")" > /dev/null; pwd)"
+
+accord_repo='https://github.com/bdeggleston/cassandra-accord.git'
+accord_branch='metadata-persistence'
+accord_src="$bin/cassandra-accord"
+
+checkout() {
+  cd "$accord_src"
+    git checkout "$accord_branch"
+    echo "$accord_branch" > .BRANCH
+  cd -
+}
+
+_main() {
+  # have we already cloned?
+  if [[ ! -e "$accord_src" ]] || [[ $(cat "$accord_src/.REPO" || true) != "$accord_repo" ]]; then
+    rm -rf "$accord_src" || true
+    git clone "$accord_repo" "$accord_src"
+    echo "$accord_repo" > "$accord_src/.REPO"
+    checkout
+  fi
+  if [[ $(cat "$accord_src"/.BRANCH || true) != "$accord_branch" ]]; then
+    checkout
+  fi
+  cd "$accord_src"
+  # are there changes?
+  git pull --rebase origin "$accord_branch"
+  if [[ $(git rev-parse HEAD) != $(cat .SHA || true) ]]; then
+    ./gradlew clean install -x test
+    git rev-parse HEAD > .SHA
+  fi
+}
+
+_main "$@"
diff --git a/.build/parent-pom-template.xml b/.build/parent-pom-template.xml
index 73a6431408..b57beb2d93 100644
--- a/.build/parent-pom-template.xml
+++ b/.build/parent-pom-template.xml
@@ -668,6 +668,11 @@
         <artifactId>jbcrypt</artifactId>
         <version>0.4</version>
       </dependency>
+      <dependency>
+        <groupId>accord</groupId>
+        <artifactId>accord</artifactId>
+        <version>1.0-SNAPSHOT</version>
+      </dependency>
       <dependency>
         <groupId>io.airlift</groupId>
         <artifactId>airline</artifactId>
diff --git a/.gitignore b/.gitignore
index c8e79b312a..91715e4aa6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -83,3 +83,4 @@ venv/
 # build-scripts will put cassandra-builds into the directory
 cassandra-builds/
 cassandra-dtest/
+.build/cassandra-accord/
diff --git a/CHANGES.txt b/CHANGES.txt
index 6d6c736e1e..b87425b419 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,6 @@
+accord
+ * CEP-15 (C*): Messaging and storage engine integration (CASSANDRA-17103)
+
 4.2
  * Prevent a user from manually removing ephemeral snapshots (CASSANDRA-17757)
  * Remove dependency on Maven Ant Tasks (CASSANDRA-17750)
diff --git a/build.xml b/build.xml
index a18fb6b72a..8628de5e8f 100644
--- a/build.xml
+++ b/build.xml
@@ -269,6 +269,11 @@
         </fileset>
     </path>
 
+    <target name="_build-accord">
+      <echo unless:set="env.ACCORD_LOCAL_DEV">building Accord, set ACCORD_LOCAL_DEV environment variable to skip</echo>
+      <exec unless:set="env.ACCORD_LOCAL_DEV" executable=".build/include-accord.sh" dir="${basedir}" logError="true" failonerror="true" failifexecutionfails="true" />
+      <echo if:set="env.ACCORD_LOCAL_DEV">ACCORD_LOCAL_DEV environment variable is set, skipping pull/install</echo>
+    </target>
   <macrodef name="create-javadoc">
     <attribute name="destdir"/>
     <element name="filesets"/>
@@ -340,7 +345,7 @@
     <!--
         Setup the output directories.
     -->
-    <target name="init" depends="validate-build-conf">
+    <target name="init" depends="validate-build-conf,_build-accord">
         <fail unless="is.source.artifact"
             message="Not a source artifact, stopping here." />
         <mkdir dir="${build.classes.main}"/>
diff --git a/simulator.sh b/simulator.sh
new file mode 100755
index 0000000000..516405e974
--- /dev/null
+++ b/simulator.sh
@@ -0,0 +1,88 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#ant jar simulator-jars
+
+DIR=`pwd`
+JVM_OPTS="$JVM_OPTS -Dcassandra.config=file://$DIR/test/conf/cassandra.yaml"
+JVM_OPTS="$JVM_OPTS -Dlogback.configurationFile=file://$DIR/test/conf/logback-simulator.xml"
+JVM_OPTS="$JVM_OPTS -Dcassandra.logdir=$DIR/build/test/logs"
+#JVM_OPTS="$JVM_OPTS -Djava.library.path=$DIR/lib/sigar-bin"
+JVM_OPTS="$JVM_OPTS -Dlegacy-sstable-root=$DIR/test/data/legacy-sstables"
+JVM_OPTS="$JVM_OPTS -Dinvalid-legacy-sstable-root=$DIR/test/data/invalid-legacy-sstables"
+JVM_OPTS="$JVM_OPTS -Dcassandra.ring_delay_ms=1000"
+JVM_OPTS="$JVM_OPTS -Dcassandra.skip_sync=true"
+JVM_OPTS="$JVM_OPTS -ea"
+JVM_OPTS="$JVM_OPTS -XX:MaxMetaspaceSize=1G"
+JVM_OPTS="$JVM_OPTS -XX:SoftRefLRUPolicyMSPerMB=0"
+JVM_OPTS="$JVM_OPTS -Dcassandra.strict.runtime.checks=true"
+JVM_OPTS="$JVM_OPTS -javaagent:$DIR/build/test/lib/jars/simulator-asm.jar"
+JVM_OPTS="$JVM_OPTS -Xbootclasspath/a:$DIR/build/test/lib/jars/simulator-bootstrap.jar"
+JVM_OPTS="$JVM_OPTS -XX:ActiveProcessorCount=4"
+JVM_OPTS="$JVM_OPTS -XX:-TieredCompilation"
+JVM_OPTS="$JVM_OPTS -XX:Tier4CompileThreshold=1000"
+JVM_OPTS="$JVM_OPTS -XX:ReservedCodeCacheSize=256M"
+JVM_OPTS="$JVM_OPTS -Xmx8G"
+JVM_OPTS="$JVM_OPTS -Dcassandra.test.simulator.determinismcheck=strict"
+JVM_OPTS="$JVM_OPTS -Dcassandra.debugrefcount=false"
+JVM_OPTS="$JVM_OPTS -Dcassandra.skip_sync=true"
+JVM_OPTS="$JVM_OPTS -Dcassandra.tolerate_sstable_size=true"
+JVM_OPTS="$JVM_OPTS -Dcassandra.test.simulator.debug=true"
+JVM_OPTS="$JVM_OPTS -Dcassandra.test.simulator.determinismcheck=strict"
+echo $JVM_OPTS
+
+CLASSPATH="$DIR"/build/test/classes
+for dir in "$DIR"/build/classes/*; do
+    CLASSPATH="$CLASSPATH:$dir"
+done
+
+for jar in "$DIR"/lib/*.jar; do
+  CLASSPATH="$CLASSPATH:$jar"
+done
+for jar in "$DIR"/build/*.jar; do
+  if [[ $jar != *"logback-classic"* ]]; then
+    CLASSPATH="$CLASSPATH:$jar"
+  fi
+done
+for jar in "$DIR"/build/lib/jars/*.jar; do
+  if [[ $jar != *"logback-classic"* ]]; then
+    CLASSPATH="$CLASSPATH:$jar"
+  fi
+done
+for jar in "$DIR"/build/test/lib/jars/*.jar; do
+  if [[ $jar != *"logback-classic"* ]]; then
+    CLASSPATH="$CLASSPATH:$jar"
+  fi
+done
+
+CLASS="org.apache.cassandra.simulator.paxos.AccordSimulationRunner"
+OPTS="run -n 3..6 -t 1000 --cluster-action-limit -1 -c 2 -s 30"
+
+echo "java -cp <...> $CLASS $OPTS $@"
+
+while true
+do
+  echo ""
+  java -cp $CLASSPATH $JVM_OPTS $CLASS $OPTS $@
+  status=$?
+  if [ $status -ne 0 ] ; then
+    exit $status
+  fi
+
+done
diff --git a/src/java/org/apache/cassandra/concurrent/SingleThreadExecutorPlus.java b/src/java/org/apache/cassandra/concurrent/SingleThreadExecutorPlus.java
index eb2827774a..553855ad7b 100644
--- a/src/java/org/apache/cassandra/concurrent/SingleThreadExecutorPlus.java
+++ b/src/java/org/apache/cassandra/concurrent/SingleThreadExecutorPlus.java
@@ -25,6 +25,7 @@ public class SingleThreadExecutorPlus extends ThreadPoolExecutorPlus implements
 {
     public static class AtLeastOnce extends AtomicBoolean implements AtLeastOnceTrigger, Runnable
     {
+        private static final long serialVersionUID = 0;  // for simulator support
         protected final SequentialExecutorPlus executor;
         protected final Runnable run;
 
diff --git a/src/java/org/apache/cassandra/concurrent/Stage.java b/src/java/org/apache/cassandra/concurrent/Stage.java
index ac609aa774..4a7552ab24 100644
--- a/src/java/org/apache/cassandra/concurrent/Stage.java
+++ b/src/java/org/apache/cassandra/concurrent/Stage.java
@@ -46,6 +46,7 @@ public enum Stage
     MUTATION          (true,  "MutationStage",         "request",  DatabaseDescriptor::getConcurrentWriters,        DatabaseDescriptor::setConcurrentWriters,        Stage::multiThreadedLowSignalStage),
     COUNTER_MUTATION  (true,  "CounterMutationStage",  "request",  DatabaseDescriptor::getConcurrentCounterWriters, DatabaseDescriptor::setConcurrentCounterWriters, Stage::multiThreadedLowSignalStage),
     VIEW_MUTATION     (true,  "ViewMutationStage",     "request",  DatabaseDescriptor::getConcurrentViewWriters,    DatabaseDescriptor::setConcurrentViewWriters,    Stage::multiThreadedLowSignalStage),
+    ACCORD            (true,  "AccordStage",           "request",  DatabaseDescriptor::getConcurrentAccordOps,      DatabaseDescriptor::setConcurrentAccordOps,      Stage::multiThreadedLowSignalStage),
     GOSSIP            (true,  "GossipStage",           "internal", () -> 1,                                         null,                                            Stage::singleThreadedStage),
     REQUEST_RESPONSE  (false, "RequestResponseStage",  "request",  FBUtilities::getAvailableProcessors,             null,                                            Stage::multiThreadedLowSignalStage),
     ANTI_ENTROPY      (false, "AntiEntropyStage",      "internal", () -> 1,                                         null,                                            Stage::singleThreadedStage),
@@ -56,7 +57,6 @@ public enum Stage
     IMMEDIATE         (false, "ImmediateStage",        "internal", () -> 0,                                         null,                                            Stage::immediateExecutor),
     PAXOS_REPAIR      (false, "PaxosRepairStage",      "internal", FBUtilities::getAvailableProcessors,             null,                                            Stage::multiThreadedStage),
     ;
-
     public final String jmxName;
     private final Supplier<ExecutorPlus> executorSupplier;
     private volatile ExecutorPlus executor;
diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java
index 0281794759..71ac5937a1 100644
--- a/src/java/org/apache/cassandra/config/Config.java
+++ b/src/java/org/apache/cassandra/config/Config.java
@@ -164,6 +164,7 @@ public class Config
 
     public int concurrent_reads = 32;
     public int concurrent_writes = 32;
+    public int concurrent_accord_operations = 32;
     public int concurrent_counter_writes = 32;
     public int concurrent_materialized_view_writes = 32;
     public int available_processors = -1;
diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index 42b5e27fba..fad05e57ed 100644
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@ -497,6 +497,9 @@ public class DatabaseDescriptor
         if (conf.concurrent_counter_writes < 2)
             throw new ConfigurationException("concurrent_counter_writes must be at least 2, but was " + conf.concurrent_counter_writes, false);
 
+        if (conf.concurrent_accord_operations < 1)
+            throw new ConfigurationException("concurrent_accord_operations must be at least 1, but was " + conf.concurrent_accord_operations, false);
+
         if (conf.concurrent_replicates != null)
             logger.warn("concurrent_replicates has been deprecated and should be removed from cassandra.yaml");
 
@@ -1976,6 +1979,20 @@ public class DatabaseDescriptor
         conf.concurrent_materialized_view_writes = concurrent_materialized_view_writes;
     }
 
+    public static int getConcurrentAccordOps()
+    {
+        return conf.concurrent_accord_operations;
+    }
+
+    public static void setConcurrentAccordOps(int concurrent_operations)
+    {
+        if (concurrent_operations < 0)
+        {
+            throw new IllegalArgumentException("Concurrent accord operations must be non-negative");
+        }
+        conf.concurrent_accord_operations = concurrent_operations;
+    }
+
     public static int getFlushWriters()
     {
         return conf.memtable_flush_writers;
diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
index ab36ec971b..8f659e6385 100644
--- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
@@ -719,7 +719,7 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa
      *
      * @return list of the mutations
      */
-    private List<? extends IMutation> getMutations(ClientState state,
+    public List<? extends IMutation> getMutations(ClientState state,
                                                    QueryOptions options,
                                                    boolean local,
                                                    long timestamp,
diff --git a/src/java/org/apache/cassandra/db/MutableDeletionInfo.java b/src/java/org/apache/cassandra/db/MutableDeletionInfo.java
index 9cad53e4ea..a695209b72 100644
--- a/src/java/org/apache/cassandra/db/MutableDeletionInfo.java
+++ b/src/java/org/apache/cassandra/db/MutableDeletionInfo.java
@@ -230,6 +230,16 @@ public class MutableDeletionInfo implements DeletionInfo
         return this;
     }
 
+    public DeletionInfo updateAllTimestampAndLocalDeletionTime(long timestamp, int localDeletionTime)
+    {
+        if (partitionDeletion.markedForDeleteAt() != Long.MIN_VALUE)
+            partitionDeletion = new DeletionTime(timestamp, localDeletionTime);
+
+        if (ranges != null)
+            ranges.updateAllTimestampAndLocalDeletionTime(timestamp, localDeletionTime);
+        return this;
+    }
+
     @Override
     public boolean equals(Object o)
     {
diff --git a/src/java/org/apache/cassandra/db/RangeTombstoneList.java b/src/java/org/apache/cassandra/db/RangeTombstoneList.java
index c8992a222b..e701fec280 100644
--- a/src/java/org/apache/cassandra/db/RangeTombstoneList.java
+++ b/src/java/org/apache/cassandra/db/RangeTombstoneList.java
@@ -320,6 +320,15 @@ public class RangeTombstoneList implements Iterable<RangeTombstone>, IMeasurable
             markedAts[i] = timestamp;
     }
 
+    public void updateAllTimestampAndLocalDeletionTime(long timestamp, int localDeletionTime)
+    {
+        for (int i = 0; i < size; i++)
+        {
+            markedAts[i] = timestamp;
+            delTimes[i] = localDeletionTime;
+        }
+    }
+
     private RangeTombstone rangeTombstone(int idx)
     {
         return new RangeTombstone(Slice.make(starts[idx], ends[idx]), new DeletionTime(markedAts[idx], delTimes[idx]));
diff --git a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java
index 64136b6099..96af39708a 100644
--- a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java
+++ b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java
@@ -48,8 +48,10 @@ import org.apache.cassandra.metrics.TableMetrics;
 import org.apache.cassandra.net.Verb;
 import org.apache.cassandra.schema.ColumnMetadata;
 import org.apache.cassandra.schema.IndexMetadata;
+import org.apache.cassandra.schema.TableId;
 import org.apache.cassandra.schema.TableMetadata;
 import org.apache.cassandra.service.*;
+import org.apache.cassandra.service.accord.api.AccordKey;
 import org.apache.cassandra.tracing.Tracing;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.btree.BTreeSet;
@@ -391,6 +393,22 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar
                       isTrackingWarnings());
     }
 
+    public SinglePartitionReadCommand withNowInSec(int nowInSec)
+    {
+        return new SinglePartitionReadCommand(isDigestQuery(),
+                                              digestVersion(),
+                                              acceptsTransient(),
+                                              metadata(),
+                                              nowInSec,
+                                              columnFilter(),
+                                              rowFilter(),
+                                              limits(),
+                                              partitionKey(),
+                                              clusteringIndexFilter(),
+                                              indexMetadata(),
+                                              isTrackingWarnings());
+    }
+
     @Override
     public DecoratedKey partitionKey()
     {
diff --git a/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java b/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java
index ddcaaed812..e1792d17f8 100644
--- a/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java
@@ -18,6 +18,7 @@
 package org.apache.cassandra.db.filter;
 
 import java.io.IOException;
+import java.util.Objects;
 
 import org.apache.cassandra.schema.ColumnMetadata;
 import org.apache.cassandra.schema.TableMetadata;
diff --git a/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java b/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java
index 18dc471ef3..9d1090df6b 100644
--- a/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java
@@ -204,20 +204,6 @@ public class ClusteringIndexNamesFilter extends AbstractClusteringIndexFilter
         return sb.toString();
     }
 
-    public boolean equals(Object o)
-    {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        ClusteringIndexNamesFilter that = (ClusteringIndexNamesFilter) o;
-        return Objects.equals(clusterings, that.clusterings) &&
-               Objects.equals(reversed, that.reversed);
-    }
-
-    public int hashCode()
-    {
-        return Objects.hash(clusterings, reversed);
-    }
-
     public Kind kind()
     {
         return Kind.NAMES;
diff --git a/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java b/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java
index b96888dfbe..bcf787fee7 100644
--- a/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.db.filter;
 import java.io.IOException;
 import java.util.List;
 import java.nio.ByteBuffer;
+import java.util.Objects;
 
 import org.apache.cassandra.schema.TableMetadata;
 import org.apache.cassandra.db.*;
diff --git a/src/java/org/apache/cassandra/db/partitions/Partition.java b/src/java/org/apache/cassandra/db/partitions/Partition.java
index 8888104d95..1daf20cd00 100644
--- a/src/java/org/apache/cassandra/db/partitions/Partition.java
+++ b/src/java/org/apache/cassandra/db/partitions/Partition.java
@@ -21,10 +21,12 @@ import java.util.NavigableSet;
 
 import javax.annotation.Nullable;
 
+import org.apache.cassandra.schema.TableId;
 import org.apache.cassandra.schema.TableMetadata;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.service.accord.api.AccordKey;
 
 /**
  * In-memory representation of a Partition.
@@ -37,6 +39,7 @@ import org.apache.cassandra.db.filter.ColumnFilter;
 public interface Partition
 {
     public TableMetadata metadata();
+
     public DecoratedKey partitionKey();
     public DeletionTime partitionLevelDeletion();
 
diff --git a/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java b/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java
index 6019d56fb8..f6fc527ff5 100644
--- a/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java
+++ b/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java
@@ -795,6 +795,15 @@ public class PartitionUpdate extends AbstractBTreePartition
             this(metadata, key, columns, initialRowCapacity, canHaveShadowedData, Rows.EMPTY_STATIC_ROW, MutableDeletionInfo.live(), BTree.empty());
         }
 
+        public Builder(TableMetadata metadata,
+                       DecoratedKey key,
+                       RegularAndStaticColumns columns,
+                       Row staticRow,
+                       int initialRowCapacity)
+        {
+            this(metadata, key, columns, initialRowCapacity, true, staticRow, MutableDeletionInfo.live(), BTree.empty());
+        }
+
         private Builder(TableMetadata metadata,
                        DecoratedKey key,
                        RegularAndStaticColumns columns,
@@ -955,6 +964,14 @@ public class PartitionUpdate extends AbstractBTreePartition
             return this;
         }
 
+        public Builder updateAllTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime)
+        {
+            deletionInfo.updateAllTimestampAndLocalDeletionTime(newTimestamp - 1, newLocalDeletionTime);
+            tree = BTree.<Row, Row>transformAndFilter(tree, (x) -> x.updateAllTimestampAndLocalDeletionTime(newTimestamp, newLocalDeletionTime));
+            staticRow = this.staticRow.updateAllTimestampAndLocalDeletionTime(newTimestamp, newLocalDeletionTime);
+            return this;
+        }
+
         @Override
         public String toString()
         {
diff --git a/src/java/org/apache/cassandra/db/rows/AbstractCell.java b/src/java/org/apache/cassandra/db/rows/AbstractCell.java
index b9b5d6044c..67263782e4 100644
--- a/src/java/org/apache/cassandra/db/rows/AbstractCell.java
+++ b/src/java/org/apache/cassandra/db/rows/AbstractCell.java
@@ -117,6 +117,13 @@ public abstract class AbstractCell<V> extends Cell<V>
         return new BufferCell(column, isTombstone() ? newTimestamp - 1 : newTimestamp, ttl(), localDeletionTime(), buffer(), path());
     }
 
+    @Override
+    public ColumnData updateAllTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime)
+    {
+        int localDeletionTime = localDeletionTime() != NO_DELETION_TIME ? newLocalDeletionTime : NO_DELETION_TIME;
+        return new BufferCell(column, isTombstone() ? newTimestamp - 1 : newTimestamp, ttl(), localDeletionTime, buffer(), path());
+    }
+
     public int dataSize()
     {
         CellPath path = path();
diff --git a/src/java/org/apache/cassandra/db/rows/BTreeRow.java b/src/java/org/apache/cassandra/db/rows/BTreeRow.java
index 125932be0f..2cdbc65ae1 100644
--- a/src/java/org/apache/cassandra/db/rows/BTreeRow.java
+++ b/src/java/org/apache/cassandra/db/rows/BTreeRow.java
@@ -442,6 +442,18 @@ public class BTreeRow extends AbstractRow
         return transformAndFilter(newInfo, newDeletion, (cd) -> cd.updateAllTimestamp(newTimestamp));
     }
 
+    @Override
+    public Row updateAllTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime)
+    {
+        LivenessInfo newInfo = primaryKeyLivenessInfo.isEmpty() ? primaryKeyLivenessInfo : primaryKeyLivenessInfo.withUpdatedTimestampAndLocalDeletionTime(newTimestamp, newLocalDeletionTime);
+        // If the deletion is shadowable and the row has a timestamp, we'll forced the deletion timestamp to be less than the row one, so we
+        // should get rid of said deletion.
+        Deletion newDeletion = deletion.isLive() || (deletion.isShadowable() && !primaryKeyLivenessInfo.isEmpty())
+                               ? Deletion.LIVE
+                               : new Deletion(new DeletionTime(newTimestamp - 1, newLocalDeletionTime), deletion.isShadowable());
+        return transformAndFilter(newInfo, newDeletion, (cd) -> cd.updateAllTimestampAndLocalDeletionTime(newTimestamp, newLocalDeletionTime));
+    }
+
     public Row withRowDeletion(DeletionTime newDeletion)
     {
         // Note that:
diff --git a/src/java/org/apache/cassandra/db/rows/ColumnData.java b/src/java/org/apache/cassandra/db/rows/ColumnData.java
index ae2770669c..ade18c64a2 100644
--- a/src/java/org/apache/cassandra/db/rows/ColumnData.java
+++ b/src/java/org/apache/cassandra/db/rows/ColumnData.java
@@ -265,6 +265,7 @@ public abstract class ColumnData implements IMeasurableMemory
      * This exists for the Paxos path, see {@link PartitionUpdate#updateAllTimestamp} for additional details.
      */
     public abstract ColumnData updateAllTimestamp(long newTimestamp);
+    public abstract ColumnData updateAllTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime);
 
     public abstract ColumnData markCounterLocalToBeCleared();
 
diff --git a/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java b/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java
index bfd7f125f7..ba019cac42 100644
--- a/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java
+++ b/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java
@@ -261,6 +261,13 @@ public class ComplexColumnData extends ColumnData implements Iterable<Cell<?>>
         return transformAndFilter(newDeletion, (cell) -> (Cell<?>) cell.updateAllTimestamp(newTimestamp));
     }
 
+    @Override
+    public ColumnData updateAllTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime)
+    {
+        DeletionTime newDeletion = complexDeletion.isLive() ? complexDeletion : new DeletionTime(newTimestamp - 1, newLocalDeletionTime);
+        return transformAndFilter(newDeletion, (cell) -> (Cell<?>) cell.updateAllTimestampAndLocalDeletionTime(newTimestamp, newLocalDeletionTime));
+    }
+
     public long maxTimestamp()
     {
         long timestamp = complexDeletion.markedForDeleteAt();
diff --git a/src/java/org/apache/cassandra/db/rows/Row.java b/src/java/org/apache/cassandra/db/rows/Row.java
index 9e8276a8d7..2ff4ae90a8 100644
--- a/src/java/org/apache/cassandra/db/rows/Row.java
+++ b/src/java/org/apache/cassandra/db/rows/Row.java
@@ -299,6 +299,8 @@ public interface Row extends Unfiltered, Iterable<ColumnData>, IMeasurableMemory
      */
     public Row updateAllTimestamp(long newTimestamp);
 
+    public Row updateAllTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime);
+
     /**
      * Returns a copy of this row with the new deletion as row deletion if it is more recent
      * than the current row deletion.
diff --git a/src/java/org/apache/cassandra/dht/ByteOrderedPartitioner.java b/src/java/org/apache/cassandra/dht/ByteOrderedPartitioner.java
index 2b0e2a2861..2c3c9aab36 100644
--- a/src/java/org/apache/cassandra/dht/ByteOrderedPartitioner.java
+++ b/src/java/org/apache/cassandra/dht/ByteOrderedPartitioner.java
@@ -128,6 +128,12 @@ public class ByteOrderedPartitioner implements IPartitioner
             return token;
         }
 
+        @Override
+        public int tokenHash()
+        {
+            return hashCode();
+        }
+
         @Override
         public double size(Token next)
         {
diff --git a/src/java/org/apache/cassandra/dht/LocalPartitioner.java b/src/java/org/apache/cassandra/dht/LocalPartitioner.java
index 127c5b7ded..ad8461962d 100644
--- a/src/java/org/apache/cassandra/dht/LocalPartitioner.java
+++ b/src/java/org/apache/cassandra/dht/LocalPartitioner.java
@@ -172,6 +172,12 @@ public class LocalPartitioner implements IPartitioner
             return prime + token.hashCode();
         }
 
+        @Override
+        public int tokenHash()
+        {
+            return hashCode();
+        }
+
         @Override
         public boolean equals(Object obj)
         {
diff --git a/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java b/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java
index 015610fb53..8a8ec55d70 100644
--- a/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java
+++ b/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java
@@ -203,6 +203,12 @@ public class Murmur3Partitioner implements IPartitioner
             return token;
         }
 
+        @Override
+        public int tokenHash()
+        {
+            return Long.hashCode(token);
+        }
+
         @Override
         public double size(Token next)
         {
diff --git a/src/java/org/apache/cassandra/dht/OrderPreservingPartitioner.java b/src/java/org/apache/cassandra/dht/OrderPreservingPartitioner.java
index 2d4def95d0..18cd94c388 100644
--- a/src/java/org/apache/cassandra/dht/OrderPreservingPartitioner.java
+++ b/src/java/org/apache/cassandra/dht/OrderPreservingPartitioner.java
@@ -208,6 +208,12 @@ public class OrderPreservingPartitioner implements IPartitioner
         {
             return ByteSource.of(token, version);
         }
+
+        @Override
+        public int tokenHash()
+        {
+            return token.hashCode();
+        }
     }
 
     public StringToken getToken(ByteBuffer key)
diff --git a/src/java/org/apache/cassandra/dht/RandomPartitioner.java b/src/java/org/apache/cassandra/dht/RandomPartitioner.java
index d02cfd58ad..a27d457b09 100644
--- a/src/java/org/apache/cassandra/dht/RandomPartitioner.java
+++ b/src/java/org/apache/cassandra/dht/RandomPartitioner.java
@@ -276,6 +276,12 @@ public class RandomPartitioner implements IPartitioner
             return new BigIntegerToken(token.add(BigInteger.ONE));
         }
 
+        @Override
+        public int tokenHash()
+        {
+            return token.hashCode();
+        }
+
         public double size(Token next)
         {
             BigIntegerToken n = (BigIntegerToken) next;
diff --git a/src/java/org/apache/cassandra/dht/Token.java b/src/java/org/apache/cassandra/dht/Token.java
index 3543dabc0e..be7bbbb113 100644
--- a/src/java/org/apache/cassandra/dht/Token.java
+++ b/src/java/org/apache/cassandra/dht/Token.java
@@ -123,6 +123,7 @@ public abstract class Token implements RingPosition<Token>, Serializable
     abstract public IPartitioner getPartitioner();
     abstract public long getHeapSize();
     abstract public Object getTokenValue();
+    abstract public int tokenHash();
 
     /**
      * Produce a weakly prefix-free byte-comparable representation of the token, i.e. such a sequence of bytes that any
diff --git a/src/java/org/apache/cassandra/metrics/Sampler.java b/src/java/org/apache/cassandra/metrics/Sampler.java
index de7f0b2e1a..b3492a44a1 100644
--- a/src/java/org/apache/cassandra/metrics/Sampler.java
+++ b/src/java/org/apache/cassandra/metrics/Sampler.java
@@ -153,6 +153,7 @@ public abstract class Sampler<T>
      */
     public static class Sample<S> implements Serializable
     {
+        private static final long serialVersionUID = 0;  // for simulator support
         public final S value;
         public final long count;
         public final long error;
diff --git a/src/java/org/apache/cassandra/net/ForwardingInfo.java b/src/java/org/apache/cassandra/net/ForwardingInfo.java
index 76e2a75372..4de3337605 100644
--- a/src/java/org/apache/cassandra/net/ForwardingInfo.java
+++ b/src/java/org/apache/cassandra/net/ForwardingInfo.java
@@ -44,6 +44,7 @@ import static org.apache.cassandra.utils.vint.VIntCoding.computeUnsignedVIntSize
  */
 public final class ForwardingInfo implements Serializable
 {
+    private static final long serialVersionUID = 0;  // for simulator support
     final List<InetAddressAndPort> targets;
     final long[] messageIds;
 
diff --git a/src/java/org/apache/cassandra/net/Message.java b/src/java/org/apache/cassandra/net/Message.java
index 472c5aacde..1ed5f03df1 100644
--- a/src/java/org/apache/cassandra/net/Message.java
+++ b/src/java/org/apache/cassandra/net/Message.java
@@ -33,6 +33,7 @@ import com.google.common.primitives.Ints;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import accord.messages.ReplyContext;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.exceptions.RequestFailureReason;
 import org.apache.cassandra.io.IVersionedAsymmetricSerializer;
@@ -68,7 +69,7 @@ import static org.apache.cassandra.utils.vint.VIntCoding.skipUnsignedVInt;
  *
  * @param <T> The type of the message payload.
  */
-public class Message<T>
+public class Message<T> implements ReplyContext
 {
     private static final Logger logger = LoggerFactory.getLogger(Message.class);
     private static final NoSpamLogger noSpam1m = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES);
diff --git a/src/java/org/apache/cassandra/net/ResponseVerbHandler.java b/src/java/org/apache/cassandra/net/ResponseVerbHandler.java
index 1cee468cd3..3518888591 100644
--- a/src/java/org/apache/cassandra/net/ResponseVerbHandler.java
+++ b/src/java/org/apache/cassandra/net/ResponseVerbHandler.java
@@ -26,7 +26,7 @@ import org.apache.cassandra.tracing.Tracing;
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime;
 
-class ResponseVerbHandler implements IVerbHandler
+public class ResponseVerbHandler implements IVerbHandler
 {
     public static final ResponseVerbHandler instance = new ResponseVerbHandler();
 
diff --git a/src/java/org/apache/cassandra/net/Verb.java b/src/java/org/apache/cassandra/net/Verb.java
index d50a187fda..f8ec811e7a 100644
--- a/src/java/org/apache/cassandra/net/Verb.java
+++ b/src/java/org/apache/cassandra/net/Verb.java
@@ -21,6 +21,7 @@ import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
 import java.util.function.Supplier;
 import java.util.function.ToLongFunction;
 
@@ -75,6 +76,11 @@ import org.apache.cassandra.schema.SchemaMutationsSerializer;
 import org.apache.cassandra.schema.SchemaPullVerbHandler;
 import org.apache.cassandra.schema.SchemaPushVerbHandler;
 import org.apache.cassandra.schema.SchemaVersionVerbHandler;
+import org.apache.cassandra.service.accord.serializers.ApplyAndCheckSerializers;
+import org.apache.cassandra.service.accord.serializers.BeginInvalidateSerializers;
+import org.apache.cassandra.service.accord.serializers.CheckStatusSerializers;
+import org.apache.cassandra.service.accord.serializers.InformOfPersistenceSerializers;
+import org.apache.cassandra.service.accord.serializers.InformOfTxnSerializers;
 import org.apache.cassandra.service.paxos.PaxosCommit;
 import org.apache.cassandra.service.paxos.PaxosCommitAndPrepare;
 import org.apache.cassandra.service.paxos.PaxosPrepare;
@@ -87,6 +93,14 @@ import org.apache.cassandra.service.paxos.cleanup.PaxosCleanupResponse;
 import org.apache.cassandra.service.paxos.cleanup.PaxosCleanupComplete;
 import org.apache.cassandra.service.paxos.cleanup.PaxosStartPrepareCleanup;
 import org.apache.cassandra.service.paxos.cleanup.PaxosFinishPrepareCleanup;
+import org.apache.cassandra.service.accord.AccordService;
+import org.apache.cassandra.service.accord.serializers.AcceptSerializers;
+import org.apache.cassandra.service.accord.serializers.ApplySerializers;
+import org.apache.cassandra.service.accord.serializers.CommitSerializers;
+import org.apache.cassandra.service.accord.serializers.PreacceptSerializers;
+import org.apache.cassandra.service.accord.serializers.ReadDataSerializers;
+import org.apache.cassandra.service.accord.serializers.RecoverySerializers;
+import org.apache.cassandra.service.accord.serializers.WaitOnCommitSerializer;
 import org.apache.cassandra.utils.BooleanSerializer;
 import org.apache.cassandra.service.EchoVerbHandler;
 import org.apache.cassandra.service.SnapshotVerbHandler;
@@ -104,39 +118,41 @@ import static org.apache.cassandra.concurrent.Stage.*;
 import static org.apache.cassandra.net.VerbTimeouts.*;
 import static org.apache.cassandra.net.Verb.Kind.*;
 import static org.apache.cassandra.net.Verb.Priority.*;
+import static org.apache.cassandra.net.ResponseHandlerSupplier.RESPONSE_HANDLER;
 
 /**
  * Note that priorities except P0 are presently unused.  P0 corresponds to urgent, i.e. what used to be the "Gossip" connection.
  */
+@SuppressWarnings("Convert2MethodRef") // we must defer all initialisation, which includes e.g. taking a method reference to a static object/singleton, which this inspection does not disambiguate
 public enum Verb
 {
-    MUTATION_RSP           (60,  P1, writeTimeout,    REQUEST_RESPONSE,  () -> NoPayload.serializer,                 () -> ResponseVerbHandler.instance                             ),
+    MUTATION_RSP           (60,  P1, writeTimeout,    REQUEST_RESPONSE,  () -> NoPayload.serializer,                 RESPONSE_HANDLER                             ),
     MUTATION_REQ           (0,   P3, writeTimeout,    MUTATION,          () -> Mutation.serializer,                  () -> MutationVerbHandler.instance,        MUTATION_RSP        ),
-    HINT_RSP               (61,  P1, writeTimeout,    REQUEST_RESPONSE,  () -> NoPayload.serializer,                 () -> ResponseVerbHandler.instance                             ),
+    HINT_RSP               (61,  P1, writeTimeout,    REQUEST_RESPONSE,  () -> NoPayload.serializer,                 RESPONSE_HANDLER                             ),
     HINT_REQ               (1,   P4, writeTimeout,    MUTATION,          () -> HintMessage.serializer,               () -> HintVerbHandler.instance,            HINT_RSP            ),
-    READ_REPAIR_RSP        (62,  P1, writeTimeout,    REQUEST_RESPONSE,  () -> NoPayload.serializer,                 () -> ResponseVerbHandler.instance                             ),
+    READ_REPAIR_RSP        (62,  P1, writeTimeout,    REQUEST_RESPONSE,  () -> NoPayload.serializer,                 RESPONSE_HANDLER                             ),
     READ_REPAIR_REQ        (2,   P1, writeTimeout,    MUTATION,          () -> Mutation.serializer,                  () -> ReadRepairVerbHandler.instance,      READ_REPAIR_RSP     ),
-    BATCH_STORE_RSP        (65,  P1, writeTimeout,    REQUEST_RESPONSE,  () -> NoPayload.serializer,                 () -> ResponseVerbHandler.instance                             ),
+    BATCH_STORE_RSP        (65,  P1, writeTimeout,    REQUEST_RESPONSE,  () -> NoPayload.serializer,                 RESPONSE_HANDLER                             ),
     BATCH_STORE_REQ        (5,   P3, writeTimeout,    MUTATION,          () -> Batch.serializer,                     () -> BatchStoreVerbHandler.instance,      BATCH_STORE_RSP     ),
-    BATCH_REMOVE_RSP       (66,  P1, writeTimeout,    REQUEST_RESPONSE,  () -> NoPayload.serializer,                 () -> ResponseVerbHandler.instance                             ),
+    BATCH_REMOVE_RSP       (66,  P1, writeTimeout,    REQUEST_RESPONSE,  () -> NoPayload.serializer,                 RESPONSE_HANDLER                             ),
     BATCH_REMOVE_REQ       (6,   P3, writeTimeout,    MUTATION,          () -> TimeUUID.Serializer.instance,         () -> BatchRemoveVerbHandler.instance,     BATCH_REMOVE_RSP    ),
 
-    PAXOS_PREPARE_RSP      (93,  P2, writeTimeout,    REQUEST_RESPONSE,  () -> PrepareResponse.serializer,           () -> ResponseVerbHandler.instance                             ),
+    PAXOS_PREPARE_RSP      (93,  P2, writeTimeout,    REQUEST_RESPONSE,  () -> PrepareResponse.serializer,           RESPONSE_HANDLER                             ),
     PAXOS_PREPARE_REQ      (33,  P2, writeTimeout,    MUTATION,          () -> Commit.serializer,                    () -> PrepareVerbHandler.instance,         PAXOS_PREPARE_RSP   ),
-    PAXOS_PROPOSE_RSP      (94,  P2, writeTimeout,    REQUEST_RESPONSE,  () -> BooleanSerializer.serializer,         () -> ResponseVerbHandler.instance                             ),
+    PAXOS_PROPOSE_RSP      (94,  P2, writeTimeout,    REQUEST_RESPONSE,  () -> BooleanSerializer.serializer,         RESPONSE_HANDLER                             ),
     PAXOS_PROPOSE_REQ      (34,  P2, writeTimeout,    MUTATION,          () -> Commit.serializer,                    () -> ProposeVerbHandler.instance,         PAXOS_PROPOSE_RSP   ),
-    PAXOS_COMMIT_RSP       (95,  P2, writeTimeout,    REQUEST_RESPONSE,  () -> NoPayload.serializer,                 () -> ResponseVerbHandler.instance                             ),
+    PAXOS_COMMIT_RSP       (95,  P2, writeTimeout,    REQUEST_RESPONSE,  () -> NoPayload.serializer,                 RESPONSE_HANDLER                             ),
     PAXOS_COMMIT_REQ       (35,  P2, writeTimeout,    MUTATION,          () -> Agreed.serializer,                    () -> PaxosCommit.requestHandler,          PAXOS_COMMIT_RSP    ),
 
-    TRUNCATE_RSP           (79,  P0, truncateTimeout, REQUEST_RESPONSE,  () -> TruncateResponse.serializer,          () -> ResponseVerbHandler.instance                             ),
+    TRUNCATE_RSP           (79,  P0, truncateTimeout, REQUEST_RESPONSE,  () -> TruncateResponse.serializer,          RESPONSE_HANDLER                             ),
     TRUNCATE_REQ           (19,  P0, truncateTimeout, MUTATION,          () -> TruncateRequest.serializer,           () -> TruncateVerbHandler.instance,        TRUNCATE_RSP        ),
 
-    COUNTER_MUTATION_RSP   (84,  P1, counterTimeout,  REQUEST_RESPONSE,  () -> NoPayload.serializer,                 () -> ResponseVerbHandler.instance                             ),
+    COUNTER_MUTATION_RSP   (84,  P1, counterTimeout,  REQUEST_RESPONSE,  () -> NoPayload.serializer,                 RESPONSE_HANDLER                             ),
     COUNTER_MUTATION_REQ   (24,  P2, counterTimeout,  COUNTER_MUTATION,  () -> CounterMutation.serializer,           () -> CounterMutationVerbHandler.instance, COUNTER_MUTATION_RSP),
 
-    READ_RSP               (63,  P2, readTimeout,     REQUEST_RESPONSE,  () -> ReadResponse.serializer,              () -> ResponseVerbHandler.instance                             ),
+    READ_RSP               (63,  P2, readTimeout,     REQUEST_RESPONSE,  () -> ReadResponse.serializer,              RESPONSE_HANDLER                             ),
     READ_REQ               (3,   P3, readTimeout,     READ,              () -> ReadCommand.serializer,               () -> ReadCommandVerbHandler.instance,     READ_RSP            ),
-    RANGE_RSP              (69,  P2, rangeTimeout,    REQUEST_RESPONSE,  () -> ReadResponse.serializer,              () -> ResponseVerbHandler.instance                             ),
+    RANGE_RSP              (69,  P2, rangeTimeout,    REQUEST_RESPONSE,  () -> ReadResponse.serializer,              RESPONSE_HANDLER                             ),
     RANGE_REQ              (9,   P3, rangeTimeout,    READ,              () -> ReadCommand.serializer,               () -> ReadCommandVerbHandler.instance,     RANGE_RSP           ),
 
     GOSSIP_DIGEST_SYN      (14,  P0, longTimeout,     GOSSIP,            () -> GossipDigestSyn.serializer,           () -> GossipDigestSynVerbHandler.instance                      ),
@@ -144,21 +160,21 @@ public enum Verb
     GOSSIP_DIGEST_ACK2     (16,  P0, longTimeout,     GOSSIP,            () -> GossipDigestAck2.serializer,          () -> GossipDigestAck2VerbHandler.instance                     ),
     GOSSIP_SHUTDOWN        (29,  P0, rpcTimeout,      GOSSIP,            () -> NoPayload.serializer,                 () -> GossipShutdownVerbHandler.instance                       ),
 
-    ECHO_RSP               (91,  P0, rpcTimeout,      GOSSIP,            () -> NoPayload.serializer,                 () -> ResponseVerbHandler.instance                             ),
+    ECHO_RSP               (91,  P0, rpcTimeout,      GOSSIP,            () -> NoPayload.serializer,                 RESPONSE_HANDLER                             ),
     ECHO_REQ               (31,  P0, rpcTimeout,      GOSSIP,            () -> NoPayload.serializer,                 () -> EchoVerbHandler.instance,            ECHO_RSP            ),
-    PING_RSP               (97,  P1, pingTimeout,     GOSSIP,            () -> NoPayload.serializer,                 () -> ResponseVerbHandler.instance                             ),
+    PING_RSP               (97,  P1, pingTimeout,     GOSSIP,            () -> NoPayload.serializer,                 RESPONSE_HANDLER                             ),
     PING_REQ               (37,  P1, pingTimeout,     GOSSIP,            () -> PingRequest.serializer,               () -> PingVerbHandler.instance,            PING_RSP            ),
 
     // P1 because messages can be arbitrarily large or aren't crucial
-    SCHEMA_PUSH_RSP        (98,  P1, rpcTimeout,      MIGRATION,         () -> NoPayload.serializer,                 () -> ResponseVerbHandler.instance                             ),
+    SCHEMA_PUSH_RSP        (98,  P1, rpcTimeout,      MIGRATION,         () -> NoPayload.serializer,                 RESPONSE_HANDLER                             ),
     SCHEMA_PUSH_REQ        (18,  P1, rpcTimeout,      MIGRATION,         () -> SchemaMutationsSerializer.instance,   () -> SchemaPushVerbHandler.instance,      SCHEMA_PUSH_RSP     ),
-    SCHEMA_PULL_RSP        (88,  P1, rpcTimeout,      MIGRATION,         () -> SchemaMutationsSerializer.instance,   () -> ResponseVerbHandler.instance                             ),
+    SCHEMA_PULL_RSP        (88,  P1, rpcTimeout,      MIGRATION,         () -> SchemaMutationsSerializer.instance,   RESPONSE_HANDLER                             ),
     SCHEMA_PULL_REQ        (28,  P1, rpcTimeout,      MIGRATION,         () -> NoPayload.serializer,                 () -> SchemaPullVerbHandler.instance,      SCHEMA_PULL_RSP     ),
-    SCHEMA_VERSION_RSP     (80,  P1, rpcTimeout,      MIGRATION,         () -> UUIDSerializer.serializer,            () -> ResponseVerbHandler.instance                             ),
+    SCHEMA_VERSION_RSP     (80,  P1, rpcTimeout,      MIGRATION,         () -> UUIDSerializer.serializer,            RESPONSE_HANDLER                             ),
     SCHEMA_VERSION_REQ     (20,  P1, rpcTimeout,      MIGRATION,         () -> NoPayload.serializer,                 () -> SchemaVersionVerbHandler.instance,   SCHEMA_VERSION_RSP  ),
 
     // repair; mostly doesn't use callbacks and sends responses as their own request messages, with matching sessions by uuid; should eventually harmonize and make idiomatic
-    REPAIR_RSP             (100, P1, repairTimeout,   REQUEST_RESPONSE,  () -> NoPayload.serializer,                 () -> ResponseVerbHandler.instance                             ),
+    REPAIR_RSP             (100, P1, repairTimeout,   REQUEST_RESPONSE,  () -> NoPayload.serializer,                 RESPONSE_HANDLER                             ),
     VALIDATION_RSP         (102, P1, longTimeout,     ANTI_ENTROPY,      () -> ValidationResponse.serializer,        () -> RepairMessageVerbHandler.instance,   REPAIR_RSP          ),
     VALIDATION_REQ         (101, P1, repairTimeout,   ANTI_ENTROPY,      () -> ValidationRequest.serializer,         () -> RepairMessageVerbHandler.instance,   REPAIR_RSP          ),
     SYNC_RSP               (104, P1, repairTimeout,   ANTI_ENTROPY,      () -> SyncResponse.serializer,              () -> RepairMessageVerbHandler.instance,   REPAIR_RSP          ),
@@ -175,35 +191,69 @@ public enum Verb
     STATUS_RSP             (115, P1, repairTimeout,   ANTI_ENTROPY,      () -> StatusResponse.serializer,            () -> RepairMessageVerbHandler.instance,   REPAIR_RSP          ),
     STATUS_REQ             (114, P1, repairTimeout,   ANTI_ENTROPY,      () -> StatusRequest.serializer,             () -> RepairMessageVerbHandler.instance,   REPAIR_RSP          ),
 
-    REPLICATION_DONE_RSP   (82,  P0, rpcTimeout,      MISC,              () -> NoPayload.serializer,                 () -> ResponseVerbHandler.instance                             ),
+    REPLICATION_DONE_RSP   (82,  P0, rpcTimeout,      MISC,              () -> NoPayload.serializer,                 RESPONSE_HANDLER                             ),
     REPLICATION_DONE_REQ   (22,  P0, rpcTimeout,      MISC,              () -> NoPayload.serializer,                 () -> ReplicationDoneVerbHandler.instance, REPLICATION_DONE_RSP),
-    SNAPSHOT_RSP           (87,  P0, rpcTimeout,      MISC,              () -> NoPayload.serializer,                 () -> ResponseVerbHandler.instance                             ),
+    SNAPSHOT_RSP           (87,  P0, rpcTimeout,      MISC,              () -> NoPayload.serializer,                 RESPONSE_HANDLER                             ),
     SNAPSHOT_REQ           (27,  P0, rpcTimeout,      MISC,              () -> SnapshotCommand.serializer,           () -> SnapshotVerbHandler.instance,        SNAPSHOT_RSP        ),
 
     PAXOS2_COMMIT_REMOTE_REQ         (38, P2, writeTimeout,  MUTATION,          () -> Mutation.serializer,                     () -> MutationVerbHandler.instance,                          MUTATION_RSP                     ),
-    PAXOS2_COMMIT_REMOTE_RSP         (39, P2, writeTimeout,  REQUEST_RESPONSE,  () -> NoPayload.serializer,                    () -> ResponseVerbHandler.instance                                                            ),
-    PAXOS2_PREPARE_RSP               (50, P2, writeTimeout,  REQUEST_RESPONSE,  () -> PaxosPrepare.responseSerializer,         () -> ResponseVerbHandler.instance                                                            ),
+    PAXOS2_COMMIT_REMOTE_RSP         (39, P2, writeTimeout,  REQUEST_RESPONSE,  () -> NoPayload.serializer,                    RESPONSE_HANDLER                                                            ),
+    PAXOS2_PREPARE_RSP               (50, P2, writeTimeout,  REQUEST_RESPONSE,  () -> PaxosPrepare.responseSerializer,         RESPONSE_HANDLER                                                            ),
     PAXOS2_PREPARE_REQ               (40, P2, writeTimeout,  MUTATION,          () -> PaxosPrepare.requestSerializer,          () -> PaxosPrepare.requestHandler,                           PAXOS2_PREPARE_RSP               ),
-    PAXOS2_PREPARE_REFRESH_RSP       (51, P2, writeTimeout,  REQUEST_RESPONSE,  () -> PaxosPrepareRefresh.responseSerializer,  () -> ResponseVerbHandler.instance                                                            ),
+    PAXOS2_PREPARE_REFRESH_RSP       (51, P2, writeTimeout,  REQUEST_RESPONSE,  () -> PaxosPrepareRefresh.responseSerializer,  RESPONSE_HANDLER                                                            ),
     PAXOS2_PREPARE_REFRESH_REQ       (41, P2, writeTimeout,  MUTATION,          () -> PaxosPrepareRefresh.requestSerializer,   () -> PaxosPrepareRefresh.requestHandler,                    PAXOS2_PREPARE_REFRESH_RSP       ),
-    PAXOS2_PROPOSE_RSP               (52, P2, writeTimeout,  REQUEST_RESPONSE,  () -> PaxosPropose.responseSerializer,         () -> ResponseVerbHandler.instance                                                            ),
+    PAXOS2_PROPOSE_RSP               (52, P2, writeTimeout,  REQUEST_RESPONSE,  () -> PaxosPropose.responseSerializer,         RESPONSE_HANDLER                                                            ),
     PAXOS2_PROPOSE_REQ               (42, P2, writeTimeout,  MUTATION,          () -> PaxosPropose.requestSerializer,          () -> PaxosPropose.requestHandler,                           PAXOS2_PROPOSE_RSP               ),
-    PAXOS2_COMMIT_AND_PREPARE_RSP    (53, P2, writeTimeout,  REQUEST_RESPONSE,  () -> PaxosPrepare.responseSerializer,         () -> ResponseVerbHandler.instance                                                            ),
+    PAXOS2_COMMIT_AND_PREPARE_RSP    (53, P2, writeTimeout,  REQUEST_RESPONSE,  () -> PaxosPrepare.responseSerializer,         RESPONSE_HANDLER                                                            ),
     PAXOS2_COMMIT_AND_PREPARE_REQ    (43, P2, writeTimeout,  MUTATION,          () -> PaxosCommitAndPrepare.requestSerializer, () -> PaxosCommitAndPrepare.requestHandler,                  PAXOS2_COMMIT_AND_PREPARE_RSP    ),
-    PAXOS2_REPAIR_RSP                (54, P2, writeTimeout,  PAXOS_REPAIR,      () -> PaxosRepair.responseSerializer,          () -> ResponseVerbHandler.instance                                                            ),
+    PAXOS2_REPAIR_RSP                (54, P2, writeTimeout,  PAXOS_REPAIR,      () -> PaxosRepair.responseSerializer,          RESPONSE_HANDLER                                                            ),
     PAXOS2_REPAIR_REQ                (44, P2, writeTimeout,  PAXOS_REPAIR,      () -> PaxosRepair.requestSerializer,           () -> PaxosRepair.requestHandler,                            PAXOS2_REPAIR_RSP                ),
-    PAXOS2_CLEANUP_START_PREPARE_RSP (55, P2, repairTimeout, PAXOS_REPAIR,      () -> PaxosCleanupHistory.serializer,          () -> ResponseVerbHandler.instance                                                            ),
+    PAXOS2_CLEANUP_START_PREPARE_RSP (55, P2, repairTimeout, PAXOS_REPAIR,      () -> PaxosCleanupHistory.serializer,          RESPONSE_HANDLER                                                            ),
     PAXOS2_CLEANUP_START_PREPARE_REQ (45, P2, repairTimeout, PAXOS_REPAIR,      () -> PaxosStartPrepareCleanup.serializer,     () -> PaxosStartPrepareCleanup.verbHandler,                  PAXOS2_CLEANUP_START_PREPARE_RSP ),
-    PAXOS2_CLEANUP_RSP               (56, P2, repairTimeout, PAXOS_REPAIR,      () -> NoPayload.serializer,                    () -> ResponseVerbHandler.instance                                                            ),
+    PAXOS2_CLEANUP_RSP               (56, P2, repairTimeout, PAXOS_REPAIR,      () -> NoPayload.serializer,                    RESPONSE_HANDLER                                                            ),
     PAXOS2_CLEANUP_REQ               (46, P2, repairTimeout, PAXOS_REPAIR,      () -> PaxosCleanupRequest.serializer,          () -> PaxosCleanupRequest.verbHandler,                       PAXOS2_CLEANUP_RSP               ),
     PAXOS2_CLEANUP_RSP2              (57, P2, repairTimeout, PAXOS_REPAIR,      () -> PaxosCleanupResponse.serializer,         () -> PaxosCleanupResponse.verbHandler                                                        ),
-    PAXOS2_CLEANUP_FINISH_PREPARE_RSP(58, P2, repairTimeout, PAXOS_REPAIR,      () -> NoPayload.serializer,                    () -> ResponseVerbHandler.instance                                                            ),
+    PAXOS2_CLEANUP_FINISH_PREPARE_RSP(58, P2, repairTimeout, PAXOS_REPAIR,      () -> NoPayload.serializer,                    RESPONSE_HANDLER                                                            ),
     PAXOS2_CLEANUP_FINISH_PREPARE_REQ(47, P2, repairTimeout, IMMEDIATE,         () -> PaxosCleanupHistory.serializer,          () -> PaxosFinishPrepareCleanup.verbHandler,                 PAXOS2_CLEANUP_FINISH_PREPARE_RSP),
-    PAXOS2_CLEANUP_COMPLETE_RSP      (59, P2, repairTimeout, PAXOS_REPAIR,      () -> NoPayload.serializer,                    () -> ResponseVerbHandler.instance                                                            ),
+    PAXOS2_CLEANUP_COMPLETE_RSP      (59, P2, repairTimeout, PAXOS_REPAIR,      () -> NoPayload.serializer,                    RESPONSE_HANDLER                                                            ),
     PAXOS2_CLEANUP_COMPLETE_REQ      (48, P2, repairTimeout, PAXOS_REPAIR,      () -> PaxosCleanupComplete.serializer,         () -> PaxosCleanupComplete.verbHandler,                      PAXOS2_CLEANUP_COMPLETE_RSP      ),
 
+    // accord
+    ACCORD_PREACCEPT_RSP            (121, P2, writeTimeout,    REQUEST_RESPONSE,  () -> PreacceptSerializers.reply,           RESPONSE_HANDLER),
+    ACCORD_PREACCEPT_REQ            (120, P2, writeTimeout,    ACCORD,            () -> PreacceptSerializers.request,         () -> AccordService.instance().verbHandler(),       ACCORD_PREACCEPT_RSP),
+
+    ACCORD_ACCEPT_RSP               (124, P2, writeTimeout,    REQUEST_RESPONSE,  () -> AcceptSerializers.reply,              RESPONSE_HANDLER),
+    ACCORD_ACCEPT_REQ               (122, P2, writeTimeout,    ACCORD,            () -> AcceptSerializers.request,            () -> AccordService.instance().verbHandler(),       ACCORD_ACCEPT_RSP   ),
+    ACCORD_ACCEPT_INVALIDATE_REQ    (123, P2, writeTimeout,    ACCORD,            () -> AcceptSerializers.invalidate,         () -> AccordService.instance().verbHandler(),       ACCORD_ACCEPT_RSP   ),
+
+    ACCORD_READ_RSP                 (128, P2, writeTimeout,    REQUEST_RESPONSE,  () -> ReadDataSerializers.reply,            RESPONSE_HANDLER),
+    ACCORD_READ_REQ                 (127, P2, writeTimeout,    ACCORD,            () -> ReadDataSerializers.request,          () -> AccordService.instance().verbHandler(),       ACCORD_READ_RSP     ),
+    ACCORD_COMMIT_REQ               (125, P2, writeTimeout,    ACCORD,            () -> CommitSerializers.request,            () -> AccordService.instance().verbHandler(), ACCORD_READ_RSP     ),
+    ACCORD_COMMIT_INVALIDATE_REQ    (126, P2, writeTimeout,    ACCORD,            () -> CommitSerializers.invalidate,         () -> AccordService.instance().verbHandler()),
+
+    ACCORD_APPLY_RSP                (130, P2, writeTimeout,    REQUEST_RESPONSE,  () -> ApplySerializers.reply,               RESPONSE_HANDLER),
+    ACCORD_APPLY_REQ                (129, P2, writeTimeout,    ACCORD,            () -> ApplySerializers.request,             () -> AccordService.instance().verbHandler(), ACCORD_APPLY_RSP),
+    ACCORD_APPLY_AND_CHECK_RSP      (132, P2, writeTimeout,    REQUEST_RESPONSE,  () -> ApplyAndCheckSerializers.reply,       RESPONSE_HANDLER),
+    ACCORD_APPLY_AND_CHECK_REQ      (131, P2, writeTimeout,    ACCORD,            () -> ApplyAndCheckSerializers.request,     () -> AccordService.instance().verbHandler(), ACCORD_APPLY_AND_CHECK_RSP),
+
+    ACCORD_RECOVER_RSP              (134, P2, writeTimeout,    REQUEST_RESPONSE,  () -> RecoverySerializers.reply,            RESPONSE_HANDLER),
+    ACCORD_RECOVER_REQ              (133, P2, writeTimeout,    ACCORD,            () -> RecoverySerializers.request,          () -> AccordService.instance().verbHandler(),       ACCORD_RECOVER_RSP  ),
+    ACCORD_BEGIN_INVALIDATE_RSP     (136, P2, writeTimeout,    REQUEST_RESPONSE,  () -> BeginInvalidateSerializers.reply,     RESPONSE_HANDLER),
+    ACCORD_BEGIN_INVALIDATE_REQ     (135, P2, writeTimeout,    ACCORD,            () -> BeginInvalidateSerializers.request,   () -> AccordService.instance().verbHandler(),ACCORD_BEGIN_INVALIDATE_RSP),
+    ACCORD_WAIT_COMMIT_RSP          (138, P2, writeTimeout,    REQUEST_RESPONSE,  () -> WaitOnCommitSerializer.reply,         RESPONSE_HANDLER),
+    ACCORD_WAIT_COMMIT_REQ          (137, P2, writeTimeout,    ACCORD,            () -> WaitOnCommitSerializer.request,       () -> AccordService.instance().verbHandler(),     ACCORD_WAIT_COMMIT_RSP),
+
+    ACCORD_INFORM_OF_TXN_RSP        (140, P2, writeTimeout, REQUEST_RESPONSE,     () -> InformOfTxnSerializers.reply,         RESPONSE_HANDLER),
+    ACCORD_INFORM_OF_TXN_REQ        (139, P2, writeTimeout,    ACCORD,            () -> InformOfTxnSerializers.request,       () -> AccordService.instance().verbHandler(),   ACCORD_INFORM_OF_TXN_RSP),
+
+    ACCORD_INFORM_OF_PERSISTENCE_RSP(142, P2, writeTimeout, REQUEST_RESPONSE,     () -> InformOfTxnSerializers.reply,         RESPONSE_HANDLER),
+    ACCORD_INFORM_OF_PERSIETENCE_REQ(141, P2, writeTimeout, ACCORD,               () -> InformOfPersistenceSerializers.request, () -> AccordService.instance().verbHandler(), ACCORD_INFORM_OF_TXN_RSP),
+
+    ACCORD_CHECK_STATUS_RSP         (144, P2, writeTimeout, REQUEST_RESPONSE,     () -> CheckStatusSerializers.reply,         RESPONSE_HANDLER),
+    ACCORD_CHECK_STATUS_REQ         (143, P2, writeTimeout, ACCORD,               () -> CheckStatusSerializers.request,       () -> AccordService.instance().verbHandler(), ACCORD_CHECK_STATUS_RSP),
+
     // generic failure response
-    FAILURE_RSP            (99,  P0, noTimeout,       REQUEST_RESPONSE,  () -> RequestFailureReason.serializer,      () -> ResponseVerbHandler.instance                             ),
+    FAILURE_RSP            (99,  P0, noTimeout,       REQUEST_RESPONSE,  () -> RequestFailureReason.serializer,      RESPONSE_HANDLER                             ),
 
     // dummy verbs
     _TRACE                 (30,  P1, rpcTimeout,      TRACING,           () -> NoPayload.serializer,                 () -> null                                                     ),
@@ -212,9 +262,9 @@ public enum Verb
     _TEST_2                (11,  P1, rpcTimeout,      IMMEDIATE,         () -> NoPayload.serializer,                 () -> null                                                     ),
 
     @Deprecated
-    REQUEST_RSP            (4,   P1, rpcTimeout,      REQUEST_RESPONSE,  () -> null,                                 () -> ResponseVerbHandler.instance                             ),
+    REQUEST_RSP            (4,   P1, rpcTimeout,      REQUEST_RESPONSE,  () -> null,                                 RESPONSE_HANDLER                             ),
     @Deprecated
-    INTERNAL_RSP           (23,  P1, rpcTimeout,      INTERNAL_RESPONSE, () -> null,                                 () -> ResponseVerbHandler.instance                             ),
+    INTERNAL_RSP           (23,  P1, rpcTimeout,      INTERNAL_RESPONSE, () -> null,                                 RESPONSE_HANDLER                             ),
 
     // largest used ID: 116
 
@@ -271,20 +321,30 @@ public enum Verb
      */
     Verb(int id, Priority priority, ToLongFunction<TimeUnit> expiration, Stage stage, Supplier<? extends IVersionedAsymmetricSerializer<?, ?>> serializer, Supplier<? extends IVerbHandler<?>> handler)
     {
-        this(id, priority, expiration, stage, serializer, handler, null);
+        this(NORMAL, id, priority, expiration, stage, serializer, handler, null, null);
+    }
+
+    Verb(int id, Priority priority, ToLongFunction<TimeUnit> expiration, Stage stage, Supplier<? extends IVersionedAsymmetricSerializer<?, ?>> serializer, Supplier<? extends IVerbHandler<?>> handler, Predicate<?> isFinalReply)
+    {
+        this(NORMAL, id, priority, expiration, stage, serializer, handler, null, isFinalReply);
     }
 
     Verb(int id, Priority priority, ToLongFunction<TimeUnit> expiration, Stage stage, Supplier<? extends IVersionedAsymmetricSerializer<?, ?>> serializer, Supplier<? extends IVerbHandler<?>> handler, Verb responseVerb)
     {
-        this(NORMAL, id, priority, expiration, stage, serializer, handler, responseVerb);
+        this(NORMAL, id, priority, expiration, stage, serializer, handler, responseVerb, null);
     }
 
     Verb(Kind kind, int id, Priority priority, ToLongFunction<TimeUnit> expiration, Stage stage, Supplier<? extends IVersionedAsymmetricSerializer<?, ?>> serializer, Supplier<? extends IVerbHandler<?>> handler)
     {
-        this(kind, id, priority, expiration, stage, serializer, handler, null);
+        this(kind, id, priority, expiration, stage, serializer, handler, null, null);
+    }
+
+    Verb(Kind kind, int id, Priority priority, ToLongFunction<TimeUnit> expiration, Stage stage, Supplier<? extends IVersionedAsymmetricSerializer<?, ?>> serializer, Supplier<? extends IVerbHandler<?>> handler, Predicate<?> isFinalReply)
+    {
+        this(kind, id, priority, expiration, stage, serializer, handler, null, isFinalReply);
     }
 
-    Verb(Kind kind, int id, Priority priority, ToLongFunction<TimeUnit> expiration, Stage stage, Supplier<? extends IVersionedAsymmetricSerializer<?, ?>> serializer, Supplier<? extends IVerbHandler<?>> handler, Verb responseVerb)
+    Verb(Kind kind, int id, Priority priority, ToLongFunction<TimeUnit> expiration, Stage stage, Supplier<? extends IVersionedAsymmetricSerializer<?, ?>> serializer, Supplier<? extends IVerbHandler<?>> handler, Verb responseVerb, Predicate<?> isFinalReply)
     {
         this.stage = stage;
         if (id < 0)
@@ -333,7 +393,7 @@ public enum Verb
     // this is a little hacky, but reduces the number of parameters up top
     public boolean isResponse()
     {
-        return handler.get() == ResponseVerbHandler.instance;
+        return handler == RESPONSE_HANDLER;
     }
 
     Verb toPre40Verb()
@@ -484,3 +544,8 @@ class VerbTimeouts
     static final ToLongFunction<TimeUnit> longTimeout     = units -> Math.max(DatabaseDescriptor.getRpcTimeout(units), units.convert(5L, TimeUnit.MINUTES));
     static final ToLongFunction<TimeUnit> noTimeout       = units -> {  throw new IllegalStateException(); };
 }
+
+class ResponseHandlerSupplier
+{
+    static final Supplier<IVerbHandler<?>> RESPONSE_HANDLER = () -> ResponseVerbHandler.instance;
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/schema/Schema.java b/src/java/org/apache/cassandra/schema/Schema.java
index f23370c549..6907010a4d 100644
--- a/src/java/org/apache/cassandra/schema/Schema.java
+++ b/src/java/org/apache/cassandra/schema/Schema.java
@@ -45,6 +45,7 @@ import org.apache.cassandra.utils.concurrent.LoadingMap;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.apache.cassandra.service.accord.AccordKeyspace;
 
 import static java.lang.String.format;
 
@@ -105,7 +106,7 @@ public class Schema implements SchemaProvider
     {
         this.online = isDaemonInitialized();
         this.localKeyspaces = (FORCE_LOAD_LOCAL_KEYSPACES || isDaemonInitialized() || isToolInitialized())
-                              ? Keyspaces.of(SchemaKeyspace.metadata(), SystemKeyspace.metadata())
+                              ? Keyspaces.of(SchemaKeyspace.metadata(), SystemKeyspace.metadata(), AccordKeyspace.metadata())
                               : Keyspaces.none();
 
         this.localKeyspaces.forEach(this::loadNew);
diff --git a/src/java/org/apache/cassandra/schema/SchemaConstants.java b/src/java/org/apache/cassandra/schema/SchemaConstants.java
index 888ea4a31f..62aae024a4 100644
--- a/src/java/org/apache/cassandra/schema/SchemaConstants.java
+++ b/src/java/org/apache/cassandra/schema/SchemaConstants.java
@@ -40,6 +40,7 @@ public final class SchemaConstants
     public static final String SCHEMA_KEYSPACE_NAME = "system_schema";
 
     public static final String TRACE_KEYSPACE_NAME = "system_traces";
+    public static final String ACCORD_KEYSPACE_NAME = "system_accord";
     public static final String AUTH_KEYSPACE_NAME = "system_auth";
     public static final String DISTRIBUTED_KEYSPACE_NAME = "system_distributed";
 
@@ -49,7 +50,7 @@ public final class SchemaConstants
 
     /* system keyspace names (the ones with LocalStrategy replication strategy) */
     public static final Set<String> LOCAL_SYSTEM_KEYSPACE_NAMES =
-        ImmutableSet.of(SYSTEM_KEYSPACE_NAME, SCHEMA_KEYSPACE_NAME);
+        ImmutableSet.of(SYSTEM_KEYSPACE_NAME, SCHEMA_KEYSPACE_NAME, ACCORD_KEYSPACE_NAME);
 
     /* virtual table system keyspace names */
     public static final Set<String> VIRTUAL_SYSTEM_KEYSPACE_NAMES =
diff --git a/src/java/org/apache/cassandra/schema/TableId.java b/src/java/org/apache/cassandra/schema/TableId.java
index fd47a47e58..53b4465bf3 100644
--- a/src/java/org/apache/cassandra/schema/TableId.java
+++ b/src/java/org/apache/cassandra/schema/TableId.java
@@ -24,6 +24,8 @@ import java.util.UUID;
 
 import org.apache.commons.lang3.ArrayUtils;
 
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.db.marshal.ValueAccessor;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID;
@@ -36,7 +38,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
  * This is essentially a UUID, but we wrap it as it's used quite a bit in the code and having a nicely named class make
  * the code more readable.
  */
-public class TableId
+public class TableId implements Comparable<TableId>
 {
     // TODO: should this be a TimeUUID?
     private final UUID id;
@@ -91,6 +93,12 @@ public class TableId
         return id;
     }
 
+    @Override
+    public int compareTo(TableId that)
+    {
+        return this.id.compareTo(that.id);
+    }
+
     @Override
     public final int hashCode()
     {
@@ -115,7 +123,15 @@ public class TableId
         out.writeLong(id.getLeastSignificantBits());
     }
 
-    public int serializedSize()
+    public <V> int serialize(V dst, ValueAccessor<V> accessor, int offset)
+    {
+        int position = offset;
+        position += accessor.putLong(dst, position, id.getMostSignificantBits());
+        position += accessor.putLong(dst, position, id.getLeastSignificantBits());
+        return position - offset;
+    }
+
+    public static int serializedSize()
     {
         return 16;
     }
@@ -124,4 +140,9 @@ public class TableId
     {
         return new TableId(new UUID(in.readLong(), in.readLong()));
     }
+
+    public static <V> TableId deserialize(V src, ValueAccessor<V> accessor, int offset) throws IOException
+    {
+        return new TableId(new UUID(accessor.getLong(src, offset), accessor.getLong(src, offset + TypeSizes.LONG_SIZE)));
+    }
 }
diff --git a/src/java/org/apache/cassandra/service/accord/AccordCallback.java b/src/java/org/apache/cassandra/service/accord/AccordCallback.java
new file mode 100644
index 0000000000..60b5d6988a
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordCallback.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.coordinate.Timeout;
+import accord.messages.Callback;
+import accord.messages.Reply;
+import org.apache.cassandra.exceptions.RequestFailureReason;
+import org.apache.cassandra.locator.InetAddressAndPort;
+import org.apache.cassandra.net.Message;
+import org.apache.cassandra.net.RequestCallback;
+
+class AccordCallback<T extends Reply> implements RequestCallback<T>
+{
+    private static final Logger logger = LoggerFactory.getLogger(AccordCallback.class);
+    private final Callback<T> callback;
+
+    public AccordCallback(Callback<T> callback)
+    {
+        this.callback = callback;
+    }
+
+    @Override
+    public void onResponse(Message<T> msg)
+    {
+        logger.debug("Received response {} from {}", msg.payload, msg.from());
+        callback.onSuccess(EndpointMapping.endpointToId(msg.from()), msg.payload);
+    }
+
+    private static Throwable convertReason(RequestFailureReason reason)
+    {
+        return reason == RequestFailureReason.TIMEOUT ?
+               new Timeout(null, null) :
+               new RuntimeException(reason.toString());
+    }
+
+    @Override
+    public void onFailure(InetAddressAndPort from, RequestFailureReason failureReason)
+    {
+        logger.debug("Received failure {} from {} for {}", failureReason, from, callback);
+        // TODO (now): we should distinguish timeout failures with some placeholder Exception
+        callback.onFailure(EndpointMapping.endpointToId(from), convertReason(failureReason));
+    }
+
+    @Override
+    public boolean invokeOnFailure()
+    {
+        return true;
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordCommand.java b/src/java/org/apache/cassandra/service/accord/AccordCommand.java
new file mode 100644
index 0000000000..3c373e60df
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordCommand.java
@@ -0,0 +1,788 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.nio.ByteBuffer;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.base.Preconditions;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.api.Data;
+import accord.api.Key;
+import accord.api.Result;
+import accord.local.Command;
+import accord.local.Listener;
+import accord.local.Listeners;
+import accord.local.PartialCommand;
+import accord.local.PreLoadContext;
+import accord.local.Status;
+import accord.primitives.Ballot;
+import accord.primitives.Deps;
+import accord.primitives.KeyRanges;
+import accord.primitives.Keys;
+import accord.primitives.Timestamp;
+import accord.primitives.TxnId;
+import accord.txn.Txn;
+import accord.txn.Writes;
+import accord.utils.DeterministicIdentitySet;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.async.AsyncContext;
+import org.apache.cassandra.service.accord.db.AccordData;
+import org.apache.cassandra.service.accord.store.StoredBoolean;
+import org.apache.cassandra.service.accord.store.StoredNavigableMap;
+import org.apache.cassandra.service.accord.store.StoredSet;
+import org.apache.cassandra.service.accord.store.StoredValue;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.concurrent.AsyncPromise;
+import org.apache.cassandra.utils.concurrent.Future;
+
+import static org.apache.cassandra.service.accord.AccordState.WriteOnly.applyMapChanges;
+import static org.apache.cassandra.service.accord.AccordState.WriteOnly.applySetChanges;
+
+public class AccordCommand extends Command implements AccordState<TxnId>
+{
+    private static final Logger logger = LoggerFactory.getLogger(AccordCommand.class);
+
+    private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger(0);
+
+    private static final long EMPTY_SIZE = ObjectSizes.measure(new AccordCommand(null, null));
+
+    public static class WriteOnly extends AccordCommand implements AccordState.WriteOnly<TxnId, AccordCommand>
+    {
+        private Future<?> future = null;
+
+        public WriteOnly(AccordCommandStore commandStore, TxnId txnId)
+        {
+            super(commandStore, txnId);
+        }
+
+        @Override
+        public void future(Future<?> future)
+        {
+            Preconditions.checkArgument(this.future == null);
+            this.future = future;
+        }
+
+        @Override
+        public Future<?> future()
+        {
+            return future;
+        }
+
+        @Override
+        public void applyChanges(AccordCommand instance)
+        {
+            applyMapChanges(this, instance, cmd -> cmd.waitingOnCommit);
+            applyMapChanges(this, instance, cmd -> cmd.waitingOnApply);
+            applySetChanges(this, instance, cmd -> cmd.blockingCommitOn);
+            applySetChanges(this, instance, cmd -> cmd.blockingApplyOn);
+        }
+    }
+
+    public static class ReadOnly extends AccordCommand implements AccordState.ReadOnly<TxnId, AccordCommand>
+    {
+        public ReadOnly(AccordCommandStore commandStore, TxnId txnId)
+        {
+            super(commandStore, txnId);
+        }
+
+        @Override
+        boolean isReadOnly()
+        {
+            return true;
+        }
+    }
+
+    private final AccordCommandStore commandStore;
+    private final TxnId txnId;
+    private final int instanceCount = INSTANCE_COUNTER.getAndIncrement();
+    public final StoredValue<Key> homeKey;
+    public final StoredValue<Key> progressKey;
+    public final StoredValue<Txn> txn;
+    public final StoredValue<Ballot> promised;
+    public final StoredValue<Ballot> accepted;
+    public final StoredValue<Timestamp> executeAt;
+    public final StoredValue<Deps> deps;
+    public final StoredValue<Writes> writes;
+    public final StoredValue<Result> result;
+
+    public final StoredValue.HistoryPreserving<Status> status;
+    public final StoredBoolean isGloballyPersistent;
+
+    public final StoredNavigableMap<TxnId, ByteBuffer> waitingOnCommit;
+    public final StoredNavigableMap<TxnId, ByteBuffer> waitingOnApply;
+
+    public final StoredSet.DeterministicIdentity<ListenerProxy> storedListeners;
+    private final Listeners transientListeners;
+
+    public final StoredSet.Navigable<TxnId> blockingCommitOn;
+    public final StoredSet.Navigable<TxnId> blockingApplyOn;
+
+    public AccordCommand(AccordCommandStore commandStore, TxnId txnId)
+    {
+        logger.trace("Instantiating new command {} @ {}", txnId, instanceHash());
+        this.commandStore = commandStore;
+        this.txnId = txnId;
+        homeKey = new StoredValue<>(kind());
+        progressKey = new StoredValue<>(kind());
+        txn = new StoredValue<>(kind());
+        promised = new StoredValue<>(kind());
+        accepted = new StoredValue<>(kind());
+        executeAt = new StoredValue<>(kind());
+        deps = new StoredValue<>(kind());
+        writes = new StoredValue<>(kind());
+        result = new StoredValue<>(kind());
+        status = new StoredValue.HistoryPreserving<>(kind());
+        isGloballyPersistent = new StoredBoolean(kind());
+        waitingOnCommit = new StoredNavigableMap<>(kind());
+        waitingOnApply = new StoredNavigableMap<>(kind());
+        storedListeners = new StoredSet.DeterministicIdentity<>(kind());
+        transientListeners = new Listeners();
+        blockingCommitOn = new StoredSet.Navigable<>(kind());
+        blockingApplyOn = new StoredSet.Navigable<>(kind());
+    }
+
+
+
+
+    @Override
+    public String toString()
+    {
+        return "AccordCommand{" +
+               "txnId=" + txnId +
+               ", instanceHash=" + instanceHash() +
+               ", status=" + status +
+               ", executeAt=" + executeAt +
+               ", promised=" + promised +
+               ", accepted=" + accepted +
+//               ", deps=" + deps +
+//               ", homeKey=" + homeKey +
+//               ", progressKey=" + progressKey +
+//               ", txn=" + txn +
+//               ", writes=" + writes +
+//               ", result=" + result +
+               ", isGloballyPersistent=" + isGloballyPersistent +
+               ", waitingOnCommit=" + waitingOnCommit +
+               ", waitingOnApply=" + waitingOnApply +
+               ", storedListeners=" + storedListeners +
+               ", transientListeners=" + transientListeners +
+               ", blockingCommitOn=" + blockingCommitOn +
+               ", blockingApplyOn=" + blockingApplyOn +
+               '}';
+    }
+
+    @Override
+    public boolean isEmpty()
+    {
+        return homeKey.isEmpty()
+               || progressKey.isEmpty()
+               || txn.isEmpty()
+               || promised.isEmpty()
+               || accepted.isEmpty()
+               || executeAt.isEmpty()
+               || deps.isEmpty()
+               || writes.isEmpty()
+               || result.isEmpty()
+               || status.isEmpty()
+               || isGloballyPersistent.isEmpty()
+               || waitingOnCommit.isEmpty()
+               || blockingCommitOn.isEmpty()
+               || waitingOnApply.isEmpty()
+               || blockingApplyOn.isEmpty()
+               || storedListeners.isEmpty();
+    }
+
+    public void setEmpty()
+    {
+        homeKey.setEmpty();
+        progressKey.setEmpty();
+        txn.setEmpty();
+        promised.setEmpty();
+        accepted.setEmpty();
+        executeAt.setEmpty();
+        deps.setEmpty();
+        writes.setEmpty();
+        result.setEmpty();
+        status.setEmpty();
+        isGloballyPersistent.setEmpty();
+        waitingOnCommit.setEmpty();
+        blockingCommitOn.setEmpty();
+        waitingOnApply.setEmpty();
+        blockingApplyOn.setEmpty();
+        storedListeners.setEmpty();;
+    }
+
+    public AccordCommand initialize()
+    {
+        logger.trace("Initializing command {} @ {}", txnId, instanceHash());
+        status.set(Status.NotWitnessed);
+        homeKey.set(null);
+        progressKey.set(null);
+        txn.set(null);
+        executeAt.load(null);
+        promised.set(Ballot.ZERO);
+        accepted.set(Ballot.ZERO);
+        deps.set(Deps.NONE);
+        writes.load(null);
+        result.load(null);
+        isGloballyPersistent.set(false);
+        waitingOnCommit.load(new TreeMap<>());
+        blockingCommitOn.load(new TreeSet<>());
+        waitingOnApply.load(new TreeMap<>());
+        blockingApplyOn.load(new TreeSet<>());
+        storedListeners.load(new DeterministicIdentitySet<>());
+        return this;
+    }
+
+    @Override
+    public boolean isLoaded()
+    {
+        return homeKey.isLoaded()
+               && progressKey.isLoaded()
+               && txn.isLoaded()
+               && promised.isLoaded()
+               && accepted.isLoaded()
+               && executeAt.isLoaded()
+               && deps.isLoaded()
+               && writes.isLoaded()
+               && result.isLoaded()
+               && status.isLoaded()
+               && isGloballyPersistent.isLoaded()
+               && waitingOnCommit.isLoaded()
+               && blockingCommitOn.isLoaded()
+               && waitingOnApply.isLoaded()
+               && blockingApplyOn.isLoaded()
+               && storedListeners.isLoaded();
+    }
+
+    public boolean isPartiallyLoaded()
+    {
+        return homeKey.isLoaded()
+               || progressKey.isLoaded()
+               || txn.isLoaded()
+               || promised.isLoaded()
+               || accepted.isLoaded()
+               || executeAt.isLoaded()
+               || deps.isLoaded()
+               || writes.isLoaded()
+               || result.isLoaded()
+               || status.isLoaded()
+               || isGloballyPersistent.isLoaded()
+               || waitingOnCommit.isLoaded()
+               || blockingCommitOn.isLoaded()
+               || waitingOnApply.isLoaded()
+               || blockingApplyOn.isLoaded()
+               || storedListeners.isLoaded();
+    }
+
+    @Override
+    public boolean hasModifications()
+    {
+        return homeKey.hasModifications()
+               || progressKey.hasModifications()
+               || txn.hasModifications()
+               || promised.hasModifications()
+               || accepted.hasModifications()
+               || executeAt.hasModifications()
+               || deps.hasModifications()
+               || writes.hasModifications()
+               || result.hasModifications()
+               || status.hasModifications()
+               || isGloballyPersistent.hasModifications()
+               || waitingOnCommit.hasModifications()
+               || blockingCommitOn.hasModifications()
+               || waitingOnApply.hasModifications()
+               || blockingApplyOn.hasModifications()
+               || storedListeners.hasModifications();
+    }
+
+    @Override
+    public void clearModifiedFlag()
+    {
+        logger.trace("Clearing modified flag on command {} @ {}", txnId, instanceHash());
+        homeKey.clearModifiedFlag();
+        progressKey.clearModifiedFlag();
+        txn.clearModifiedFlag();
+        promised.clearModifiedFlag();
+        accepted.clearModifiedFlag();
+        executeAt.clearModifiedFlag();
+        deps.clearModifiedFlag();
+        writes.clearModifiedFlag();
+        result.clearModifiedFlag();
+        status.clearModifiedFlag();
+        isGloballyPersistent.clearModifiedFlag();
+        waitingOnCommit.clearModifiedFlag();
+        blockingCommitOn.clearModifiedFlag();
+        waitingOnApply.clearModifiedFlag();
+        blockingApplyOn.clearModifiedFlag();
+        storedListeners.clearModifiedFlag();;
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        AccordCommand command = (AccordCommand) o;
+        return commandStore == command.commandStore
+               && homeKey.equals(command.homeKey)
+               && progressKey.equals(command.progressKey)
+               && txnId.equals(command.txnId)
+               && txn.equals(command.txn)
+               && promised.equals(command.promised)
+               && accepted.equals(command.accepted)
+               && executeAt.equals(command.executeAt)
+               && deps.equals(command.deps)
+               && writes.equals(command.writes)
+               && result.equals(command.result)
+               && status.equals(command.status)
+               && isGloballyPersistent.equals(command.isGloballyPersistent)
+               && waitingOnCommit.equals(command.waitingOnCommit)
+               && blockingCommitOn.equals(command.blockingCommitOn)
+               && waitingOnApply.equals(command.waitingOnApply)
+               && blockingApplyOn.equals(command.blockingApplyOn)
+               && storedListeners.equals(command.storedListeners)
+               && transientListeners.equals(command.transientListeners);
+    }
+
+    boolean isReadOnly()
+    {
+        return false;
+    }
+
+    private int instanceHash()
+    {
+//        return System.identityHashCode(this);
+        return instanceCount;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash(commandStore,
+                            txnId,
+                            homeKey,
+                            progressKey,
+                            txn,
+                            promised,
+                            accepted,
+                            executeAt,
+                            deps,
+                            writes,
+                            result,
+                            status,
+                            isGloballyPersistent,
+                            waitingOnCommit,
+                            blockingCommitOn,
+                            waitingOnApply,
+                            blockingApplyOn,
+                            storedListeners,
+                            transientListeners);
+    }
+
+    private AccordStateCache.Instance<TxnId, AccordCommand> cache()
+    {
+        return commandStore.commandCache();
+    }
+
+    @Override
+    public TxnId key()
+    {
+        return txnId;
+    }
+
+    @Override
+    public long estimatedSizeOnHeap()
+    {
+        long size = EMPTY_SIZE;
+        size += AccordObjectSizes.timestamp(txnId);
+        size += homeKey.estimatedSizeOnHeap(AccordObjectSizes::key);
+        size += progressKey.estimatedSizeOnHeap(AccordObjectSizes::key);
+        size += txn.estimatedSizeOnHeap(AccordObjectSizes::txn);
+        size += promised.estimatedSizeOnHeap(AccordObjectSizes::timestamp);
+        size += accepted.estimatedSizeOnHeap(AccordObjectSizes::timestamp);
+        size += executeAt.estimatedSizeOnHeap(AccordObjectSizes::timestamp);
+        size += deps.estimatedSizeOnHeap(AccordObjectSizes::dependencies);
+        size += writes.estimatedSizeOnHeap(AccordObjectSizes::writes);
+        size += result.estimatedSizeOnHeap(r -> ((AccordData) r).estimatedSizeOnHeap());
+        size += status.estimatedSizeOnHeap(s -> 0);
+        size += isGloballyPersistent.estimatedSizeOnHeap();
+        size += waitingOnCommit.estimatedSizeOnHeap(AccordObjectSizes::timestamp, ByteBufferUtil::estimatedSizeOnHeap);
+        size += blockingCommitOn.estimatedSizeOnHeap(AccordObjectSizes::timestamp);
+        size += waitingOnApply.estimatedSizeOnHeap(AccordObjectSizes::timestamp, ByteBufferUtil::estimatedSizeOnHeap);
+        size += blockingApplyOn.estimatedSizeOnHeap(AccordObjectSizes::timestamp);
+        size += storedListeners.estimatedSizeOnHeap(ListenerProxy::estimatedSizeOnHeap);
+        return size;
+    }
+
+    public boolean shouldUpdateDenormalizedWaitingOn()
+    {
+        if (blockingCommitOn.getView().isEmpty() && blockingApplyOn.getView().isEmpty())
+            return false;
+        return AccordPartialCommand.serializer.needsUpdate(this);
+    }
+
+    @Override
+    public TxnId txnId()
+    {
+        return txnId;
+    }
+
+    @Override
+    public AccordCommandStore commandStore()
+    {
+        return commandStore;
+    }
+
+    @Override
+    public Key homeKey()
+    {
+        return homeKey.get();
+    }
+
+    @Override
+    protected void setHomeKey(Key key)
+    {
+        homeKey.set(key);
+    }
+
+    @Override
+    public Key progressKey()
+    {
+        return progressKey.get();
+    }
+
+    @Override
+    protected void setProgressKey(Key key)
+    {
+        progressKey.set(key);
+    }
+
+    @Override
+    public Txn txn()
+    {
+        return txn.get();
+    }
+
+    @Override
+    protected void setTxn(Txn txn)
+    {
+        this.txn.set(txn);
+    }
+
+    @Override
+    public Ballot promised()
+    {
+        return promised.get();
+    }
+
+    @Override
+    public void promised(Ballot ballot)
+    {
+        this.promised.set(ballot);
+    }
+
+    @Override
+    public Ballot accepted()
+    {
+        return accepted.get();
+    }
+
+    @Override
+    public void accepted(Ballot ballot)
+    {
+        this.accepted.set(ballot);
+    }
+
+    @Override
+    public Timestamp executeAt()
+    {
+        return executeAt.get();
+    }
+
+    @Override
+    public void executeAt(Timestamp timestamp)
+    {
+        Preconditions.checkState(!status().hasBeen(Status.Committed) || executeAt().equals(timestamp));
+        this.executeAt.set(timestamp);
+    }
+
+    @Override
+    public Deps savedDeps()
+    {
+        return deps.get();
+    }
+
+    @Override
+    public void savedDeps(Deps deps)
+    {
+        this.deps.set(deps);
+    }
+
+    @Override
+    public Writes writes()
+    {
+        return writes.get();
+    }
+
+    @Override
+    public void writes(Writes writes)
+    {
+        this.writes.set(writes);
+    }
+
+    @Override
+    public Result result()
+    {
+        return result.get();
+    }
+
+    @Override
+    public void result(Result result)
+    {
+        this.result.set(result);
+    }
+
+    @Override
+    public Status status()
+    {
+        return status.get();
+    }
+
+    @Override
+    public void status(Status status)
+    {
+        this.status.set(status);
+    }
+
+    @Override
+    public boolean isGloballyPersistent()
+    {
+        return isGloballyPersistent.get();
+    }
+
+    @Override
+    public void isGloballyPersistent(boolean v)
+    {
+        isGloballyPersistent.set(v);
+    }
+
+    @Override
+    protected void postApply()
+    {
+        cache().cleanupWriteFuture(txnId);
+        super.postApply();
+    }
+
+    private boolean canApplyWithCurrentScope()
+    {
+        KeyRanges ranges = commandStore.ranges().at(executeAt().epoch);
+        Keys keys = txn().keys();
+        for (int i=0,mi=keys.size(); i<mi; i++)
+        {
+            Key key = keys.get(i);
+            if (commandStore.isCommandsForKeyInContext((AccordKey.PartitionKey) key))
+                continue;
+
+            if (!commandStore.hashIntersects(key))
+                continue;
+            if (!ranges.contains(key))
+                continue;
+
+            return false;
+        }
+        return true;
+    }
+
+    private Future<Void> applyWithCorrectScope()
+    {
+        TxnId txnId = txnId();
+        AsyncPromise<Void> promise = new AsyncPromise<>();
+        commandStore().process(this, commandStore -> {
+            AccordCommand command = (AccordCommand) commandStore.command(txnId);
+            command.apply(false).addCallback((v, throwable) -> {
+                if (throwable != null)
+                    promise.tryFailure(throwable);
+                else
+                    promise.trySuccess(null);
+            });
+        });
+        return promise;
+    }
+
+    private Future<Void> apply(boolean canReschedule)
+    {
+        Future<Void> future = cache().getWriteFuture(txnId);
+        if (future != null)
+            return future;
+
+        // this can be called via a listener callback, in which case we won't
+        // have the appropriate commandsForKey in scope, so start a new operation
+        // with the correct scope and notify the caller when that completes
+        if (!canApplyWithCurrentScope())
+        {
+            Preconditions.checkArgument(canReschedule);
+            return applyWithCorrectScope();
+        }
+
+        future = super.apply();
+        cache().setWriteFuture(txnId, future);
+        return future;
+    }
+
+    @Override
+    public Future<Void> apply()
+    {
+        return apply(true);
+    }
+
+    @Override
+    public Future<Data> read(Keys scope)
+    {
+        ReadFuture future = cache().getReadFuture(txnId);
+        if (future != null)
+            return future.scope.equals(scope) ? future : super.read(scope);
+        future = new ReadFuture(scope, super.read(scope));
+        cache().setReadFuture(txnId, future);
+        return future;
+    }
+
+    private Listener maybeWrapListener(Listener listener)
+    {
+        if (listener.isTransient())
+            return listener;
+
+        if (listener instanceof AccordCommand)
+            return new ListenerProxy.CommandListenerProxy(commandStore, ((AccordCommand) listener).txnId());
+
+        if (listener instanceof AccordCommandsForKey)
+            return new ListenerProxy.CommandsForKeyListenerProxy(commandStore, ((AccordCommandsForKey) listener).key());
+
+        throw new RuntimeException("Unhandled non-transient listener: " + listener);
+    }
+
+    @Override
+    public Command addListener(Listener listener)
+    {
+        listener = maybeWrapListener(listener);
+        if (listener instanceof ListenerProxy)
+            storedListeners.blindAdd((ListenerProxy) listener);
+        else
+            transientListeners.add(listener);
+        return this;
+    }
+
+    @Override
+    public void removeListener(Listener listener)
+    {
+        listener = maybeWrapListener(listener);
+        if (listener instanceof ListenerProxy)
+            storedListeners.blindRemove((ListenerProxy) listener);
+        else
+            transientListeners.remove(listener);
+    }
+
+    public boolean hasListenerFor(TxnId txnId)
+    {
+        return storedListeners.getView().contains(new ListenerProxy.CommandListenerProxy(commandStore, txnId));
+    }
+
+    @Override
+    public void notifyListeners()
+    {
+        storedListeners.getView().forEach(this);
+        transientListeners.forEach(listener -> {
+            PreLoadContext ctx = listener.listenerPreLoadContext(txnId());
+            AsyncContext context = commandStore().getContext();
+            if (context.containsScopedItems(ctx))
+            {
+                logger.trace("{}: synchronously updating listener {}", txnId(), listener);
+                listener.onChange(this);
+            }
+            else
+            {
+                logger.trace("{}: asynchronously updating listener {}", txnId(), listener);
+                commandStore().process(ctx, instance -> {
+                    listener.onChange(instance.command(txnId()));
+                });
+            }
+        });
+    }
+
+    @Override
+    public void addWaitingOnCommit(Command command)
+    {
+        waitingOnCommit.blindPut(command.txnId(), AccordPartialCommand.serializer.serialize(command));
+    }
+
+    @Override
+    public boolean isWaitingOnCommit()
+    {
+        return !waitingOnCommit.getView().isEmpty();
+    }
+
+    @Override
+    public void removeWaitingOnCommit(PartialCommand command)
+    {
+        waitingOnCommit.blindRemove(command.txnId());
+    }
+
+    @Override
+    public PartialCommand firstWaitingOnCommit()
+    {
+        if (!isWaitingOnCommit())
+            return null;
+        ByteBuffer bytes = waitingOnCommit.getView().firstEntry().getValue();
+        return AccordPartialCommand.serializer.deserialize(commandStore, bytes);
+    }
+
+    @Override
+    public void addWaitingOnApplyIfAbsent(PartialCommand command)
+    {
+        waitingOnApply.blindPut(command.txnId(), AccordPartialCommand.serializer.serialize(command));
+    }
+
+    @Override
+    public boolean isWaitingOnApply()
+    {
+        return !waitingOnApply.getView().isEmpty();
+    }
+
+    @Override
+    public void removeWaitingOnApply(PartialCommand command)
+    {
+        waitingOnApply.blindRemove(command.txnId());
+    }
+
+    @Override
+    public PartialCommand firstWaitingOnApply()
+    {
+        if (!isWaitingOnApply())
+            return null;
+        ByteBuffer bytes = waitingOnApply.getView().firstEntry().getValue();
+        return AccordPartialCommand.serializer.deserialize(commandStore, bytes);
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordCommandStore.java b/src/java/org/apache/cassandra/service/accord/AccordCommandStore.java
new file mode 100644
index 0000000000..f8659e0b3c
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordCommandStore.java
@@ -0,0 +1,291 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.LongSupplier;
+
+import com.google.common.base.Preconditions;
+
+import accord.api.Agent;
+import accord.api.DataStore;
+import accord.api.Key;
+import accord.api.ProgressLog;
+import accord.local.Command;
+import accord.local.CommandStore;
+import accord.local.CommandsForKey;
+import accord.local.PreLoadContext;
+import accord.primitives.Timestamp;
+import accord.primitives.TxnId;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.service.accord.async.AsyncContext;
+import org.apache.cassandra.service.accord.async.AsyncOperation;
+import org.apache.cassandra.utils.Clock;
+import org.apache.cassandra.utils.concurrent.AsyncPromise;
+import org.apache.cassandra.utils.concurrent.Future;
+import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
+
+public class AccordCommandStore extends CommandStore
+{
+    public static long maxCacheSize()
+    {
+        return 5 << 20; // TODO: make configurable
+    }
+
+    private static long getThreadId(ExecutorService executor)
+    {
+        try
+        {
+            return executor.submit(() -> Thread.currentThread().getId()).get();
+        }
+        catch (InterruptedException e)
+        {
+            throw new AssertionError(e);
+        }
+        catch (ExecutionException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private final long threadId;
+    public final String loggingId;
+    private final ExecutorService executor;
+    private final AccordStateCache stateCache;
+    private final AccordStateCache.Instance<TxnId, AccordCommand> commandCache;
+    private final AccordStateCache.Instance<PartitionKey, AccordCommandsForKey> commandsForKeyCache;
+    private AsyncContext currentCtx = null;
+    private long lastSystemTimestampMicros = Long.MIN_VALUE;
+
+    public AccordCommandStore(int generation,
+                              int index,
+                              int numShards,
+                              Function<Timestamp, Timestamp> uniqueNow,
+                              LongSupplier currentEpoch,
+                              Agent agent,
+                              DataStore store,
+                              ProgressLog.Factory progressLogFactory,
+                              RangesForEpoch rangesForEpoch,
+                              ExecutorService executor)
+    {
+        super(generation, index, numShards, uniqueNow, currentEpoch, agent, store, progressLogFactory, rangesForEpoch);
+        this.loggingId = String.format("[%s:%s]", generation, index);
+        this.executor = executor;
+        this.threadId = getThreadId(executor);
+        this.stateCache = new AccordStateCache(maxCacheSize() / numShards);
+        this.commandCache = stateCache.instance(TxnId.class,
+                                                AccordCommand.class,
+                                                txnId -> new AccordCommand(this, txnId));
+        this.commandsForKeyCache = stateCache.instance(PartitionKey.class,
+                                                       AccordCommandsForKey.class,
+                                                       key -> new AccordCommandsForKey(this, key));
+    }
+
+    void setCacheSize(long bytes)
+    {
+        checkInStoreThread();
+        stateCache.setMaxSize(bytes);
+    }
+
+    public void checkInStoreThread()
+    {
+        Preconditions.checkState(Thread.currentThread().getId() == threadId);
+    }
+
+    public void checkNotInStoreThread()
+    {
+        Preconditions.checkState(Thread.currentThread().getId() != threadId);
+    }
+
+    public ExecutorService executor()
+    {
+        return executor;
+    }
+
+    public AccordStateCache.Instance<TxnId, AccordCommand> commandCache()
+    {
+        return commandCache;
+    }
+
+    public AccordStateCache.Instance<PartitionKey, AccordCommandsForKey> commandsForKeyCache()
+    {
+        return commandsForKeyCache;
+    }
+
+    public void setContext(AsyncContext context)
+    {
+        Preconditions.checkState(currentCtx == null);
+        currentCtx = context;
+    }
+
+    public AsyncContext getContext()
+    {
+        Preconditions.checkState(currentCtx != null);
+        return currentCtx;
+    }
+
+    public void unsetContext(AsyncContext context)
+    {
+        Preconditions.checkState(currentCtx == context);
+        currentCtx = null;
+    }
+
+    public long nextSystemTimestampMicros()
+    {
+        lastSystemTimestampMicros = Math.max(TimeUnit.MILLISECONDS.toMicros(Clock.Global.currentTimeMillis()), lastSystemTimestampMicros + 1);
+        return lastSystemTimestampMicros;
+    }
+
+    private AccordCommand getCommandInternal(TxnId txnId)
+    {
+        Preconditions.checkState(currentCtx != null);
+        AccordCommand command = currentCtx.commands.get(txnId);
+        if (command == null)
+            throw new IllegalArgumentException("No command in context for txnId " + txnId);
+
+        Preconditions.checkState(command.isLoaded() || (command.isReadOnly() && command.isPartiallyLoaded()));
+
+        return command;
+    }
+
+    @Override
+    public Command command(TxnId txnId)
+    {
+        AccordCommand command = getCommandInternal(txnId);
+        if (command.isEmpty()) command.initialize();
+        return command;
+    }
+
+    @Override
+    public Command ifPresent(TxnId txnId)
+    {
+        AccordCommand command = getCommandInternal(txnId);
+        return !command.isEmpty() ? command : null;
+    }
+
+    public boolean isCommandsForKeyInContext(PartitionKey key)
+    {
+        return currentCtx.commandsForKey.get(key) != null;
+    }
+
+    private AccordCommandsForKey getCommandsForKeyInternal(Key key)
+    {
+        Preconditions.checkState(currentCtx != null);
+        Preconditions.checkArgument(key instanceof PartitionKey);
+        AccordCommandsForKey commandsForKey = currentCtx.commandsForKey.get((PartitionKey) key);
+        if (commandsForKey == null)
+            throw new IllegalArgumentException("No commandsForKey in context for key " + key);
+        Preconditions.checkState(commandsForKey.isLoaded());
+        return commandsForKey;
+    }
+
+    @Override
+    public CommandsForKey commandsForKey(Key key)
+    {
+        AccordCommandsForKey commandsForKey = getCommandsForKeyInternal(key);
+        if (commandsForKey.isEmpty())
+            commandsForKey.initialize();
+        return commandsForKey;
+    }
+
+    @Override
+    public CommandsForKey maybeCommandsForKey(Key key)
+    {
+        AccordCommandsForKey commandsForKey = getCommandsForKeyInternal(key);
+        return !commandsForKey.isEmpty() ? commandsForKey : null;
+    }
+
+    @Override
+    public Future<Void> processSetup(Consumer<? super CommandStore> function)
+    {
+        AsyncPromise<Void> promise = new AsyncPromise<>();
+        executor.execute(() -> {
+            try
+            {
+                function.accept(this);
+                promise.trySuccess(null);
+            }
+            catch (Throwable t)
+            {
+                promise.tryFailure(t);
+            }
+        });
+        return promise;
+    }
+
+    @Override
+    public <T> Future<T> processSetup(Function<? super CommandStore, T> function)
+    {
+        AsyncPromise<T> promise = new AsyncPromise<>();
+        executor.execute(() -> {
+            try
+            {
+                T result = function.apply(this);
+                promise.trySuccess(result);
+            }
+            catch (Throwable t)
+            {
+                promise.tryFailure(t);
+            }
+        });
+        return promise;
+    }
+
+    public void processBlocking(Runnable runnable)
+    {
+        try
+        {
+            executor.submit(runnable).get();
+        }
+        catch (InterruptedException e)
+        {
+            throw new UncheckedInterruptedException(e);
+        }
+        catch (ExecutionException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public <T> Future<T> process(PreLoadContext loadCtx, Function<? super CommandStore, T> function)
+    {
+        AsyncOperation<T> operation = AsyncOperation.create(this, loadCtx, function);
+        executor.execute(operation);
+        return operation;
+    }
+
+    @Override
+    public Future<Void> process(PreLoadContext loadCtx, Consumer<? super CommandStore> consumer)
+    {
+        AsyncOperation<Void> operation = AsyncOperation.create(this, loadCtx, consumer);
+        executor.execute(operation);
+        return operation;
+    }
+
+    @Override
+    public void shutdown()
+    {
+        // executors are shutdown by AccordCommandStores
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordCommandStores.java b/src/java/org/apache/cassandra/service/accord/AccordCommandStores.java
new file mode 100644
index 0000000000..3a1cbb8064
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordCommandStores.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.util.concurrent.ExecutorService;
+
+import accord.api.Agent;
+import accord.api.DataStore;
+import accord.api.ProgressLog;
+import accord.local.CommandStore;
+import accord.local.CommandStores;
+import accord.local.Node;
+import org.apache.cassandra.concurrent.ExecutorFactory;
+import org.apache.cassandra.utils.ExecutorUtils;
+
+public class AccordCommandStores extends CommandStores
+{
+    private final ExecutorService[] executors;
+
+    public AccordCommandStores(int numShards, Node node, Agent agent, DataStore store,
+                               ProgressLog.Factory progressLogFactory)
+    {
+        super(numShards, node, agent, store, progressLogFactory);
+        this.executors = new ExecutorService[numShards];
+        for (int i=0; i<numShards; i++)
+        {
+            executors[i] = ExecutorFactory.Global.executorFactory().sequential(CommandStore.class.getSimpleName() + '[' + node + ':' + i + ']');
+        }
+    }
+
+    @Override
+    protected CommandStore createCommandStore(int generation, int index, int numShards, Node node, Agent agent, DataStore store, ProgressLog.Factory progressLogFactory, CommandStore.RangesForEpoch rangesForEpoch)
+    {
+        return new AccordCommandStore(generation, index, numShards, node::uniqueNow, node.topology()::epoch, agent, store, progressLogFactory, rangesForEpoch, executors[index]);
+    }
+
+    void setCacheSize(long bytes)
+    {
+        setup(commandStore -> ((AccordCommandStore) commandStore).setCacheSize(bytes));
+    }
+
+    @Override
+    public synchronized void shutdown()
+    {
+        super.shutdown();
+        //TODO shutdown isn't useful by itself, we need a way to "wait" as well.  Should be AutoCloseable or offer awaitTermination as well (think Shutdownable interface)
+        ExecutorUtils.shutdown(executors);
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordCommandsForKey.java b/src/java/org/apache/cassandra/service/accord/AccordCommandsForKey.java
new file mode 100644
index 0000000000..54ce46a3f9
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordCommandsForKey.java
@@ -0,0 +1,459 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import com.google.common.base.Preconditions;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.local.Command;
+import accord.local.CommandStore;
+import accord.local.CommandsForKey;
+import accord.local.PartialCommand;
+import accord.local.Status;
+import accord.primitives.Timestamp;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.service.accord.store.StoredLong;
+import org.apache.cassandra.service.accord.store.StoredNavigableMap;
+import org.apache.cassandra.service.accord.store.StoredSet;
+import org.apache.cassandra.service.accord.store.StoredValue;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.concurrent.Future;
+
+import static org.apache.cassandra.service.accord.AccordState.WriteOnly.applyMapChanges;
+import static org.apache.cassandra.service.accord.AccordState.WriteOnly.applySetChanges;
+
+public class AccordCommandsForKey extends CommandsForKey implements AccordState<PartitionKey>
+{
+    private static final Logger logger = LoggerFactory.getLogger(AccordCommandsForKey.class);
+
+    private static final long EMPTY_SIZE = ObjectSizes.measureDeep(new AccordCommandsForKey(null, null));
+
+    public static class Defaults
+    {
+        public static final Timestamp maxTimestamp = Timestamp.NONE;
+        public static final Timestamp lastExecutedTimestamp = Timestamp.NONE;
+        public static final Timestamp lastWriteTimestamp = Timestamp.NONE;
+        public static final long lastExecutedMicros = 0;
+    }
+
+    public static class WriteOnly extends AccordCommandsForKey implements AccordState.WriteOnly<PartitionKey, AccordCommandsForKey>
+    {
+        private Future<?> future = null;
+
+        public WriteOnly(AccordCommandStore commandStore, PartitionKey key)
+        {
+            super(commandStore, key);
+        }
+
+        @Override
+        public void future(Future<?> future)
+        {
+            Preconditions.checkArgument(this.future == null);
+            this.future = future;
+
+        }
+
+        @Override
+        public Future<?> future()
+        {
+            return future;
+        }
+
+        @Override
+        public void applyChanges(AccordCommandsForKey instance)
+        {
+            applySetChanges(this, instance, cfk -> cfk.blindWitnessed);
+            applyMapChanges(this, instance, cfk -> cfk.uncommitted.map);
+            applyMapChanges(this, instance, cfk -> cfk.committedById.map);
+            applyMapChanges(this, instance, cfk -> cfk.committedByExecuteAt.map);
+        }
+    }
+
+    public enum SeriesKind
+    {
+        UNCOMMITTED(Command::txnId),
+        COMMITTED_BY_ID(Command::txnId),
+        COMMITTED_BY_EXECUTE_AT(Command::executeAt);
+
+        private final Function<Command, Timestamp> getTimestamp;
+
+        SeriesKind(Function<Command, Timestamp> timestampFunction)
+        {
+            this.getTimestamp = timestampFunction;
+        }
+    }
+
+    public class Series implements CommandTimeseries
+    {
+        public final SeriesKind kind;
+        public final StoredNavigableMap<Timestamp, ByteBuffer> map;
+
+        public Series(AccordState.Kind stateKind, SeriesKind kind)
+        {
+            this.kind = kind;
+            map = new StoredNavigableMap<>(stateKind);
+        }
+
+        @Override
+        public PartialCommand.WithDeps get(Timestamp timestamp)
+        {
+            ByteBuffer bytes = map.getView().get(timestamp);
+            if (bytes == null)
+                return null;
+            return AccordPartialCommand.WithDeps.serializer.deserialize(commandStore, bytes);
+        }
+
+        @Override
+        public void add(Timestamp timestamp, Command command)
+        {
+            map.blindPut(timestamp, AccordPartialCommand.WithDeps.serializer.serialize(command));
+        }
+
+        @Override
+        public void remove(Timestamp timestamp)
+        {
+            map.blindRemove(timestamp);
+        }
+
+        private Stream<PartialCommand.WithDeps> idsToCommands(Collection<ByteBuffer> blobs)
+        {
+            return blobs.stream().map(blob -> AccordPartialCommand.WithDeps.serializer.deserialize(commandStore, blob));
+        }
+
+        @Override
+        public boolean isEmpty()
+        {
+            return map.getView().isEmpty();
+        }
+
+        @Override
+        public Stream<PartialCommand.WithDeps> before(Timestamp timestamp)
+        {
+            return idsToCommands(map.getView().headMap(timestamp, false).values());
+        }
+
+        @Override
+        public Stream<PartialCommand.WithDeps> after(Timestamp timestamp)
+        {
+            return idsToCommands(map.getView().tailMap(timestamp, false).values());
+        }
+
+        @Override
+        public Stream<PartialCommand.WithDeps> between(Timestamp min, Timestamp max)
+        {
+            return idsToCommands(map.getView().subMap(min, true, max, true).values());
+        }
+
+        @Override
+        public Stream<PartialCommand.WithDeps> all()
+        {
+            return idsToCommands(map.getView().values());
+        }
+    }
+
+    private final AccordCommandStore commandStore;
+    private final PartitionKey key;
+    public final StoredValue<Timestamp> maxTimestamp;
+    public final StoredValue<Timestamp> lastExecutedTimestamp;
+    public final StoredLong lastExecutedMicros;
+    public final StoredValue<Timestamp> lastWriteTimestamp;
+    public final StoredSet.Navigable<Timestamp> blindWitnessed;
+    public final Series uncommitted;
+    public final Series committedById;
+    public final Series committedByExecuteAt;
+
+    public AccordCommandsForKey(AccordCommandStore commandStore, PartitionKey key)
+    {
+        this.commandStore = commandStore;
+        this.key = key;
+        maxTimestamp = new StoredValue<>(kind());
+        lastExecutedTimestamp = new StoredValue<>(kind());
+        lastExecutedMicros = new StoredLong(kind());
+        lastWriteTimestamp = new StoredValue<>(kind());
+        blindWitnessed = new StoredSet.Navigable<>(kind());
+        uncommitted = new Series(kind(), SeriesKind.UNCOMMITTED);
+        committedById = new Series(kind(), SeriesKind.COMMITTED_BY_ID);
+        committedByExecuteAt = new Series(kind(), SeriesKind.COMMITTED_BY_EXECUTE_AT);
+    }
+
+    @Override
+    public boolean isEmpty()
+    {
+        return maxTimestamp.isEmpty()
+               && lastExecutedTimestamp.isEmpty()
+               && lastExecutedMicros.isEmpty()
+               && lastWriteTimestamp.isEmpty()
+               && blindWitnessed.isEmpty()
+               && uncommitted.map.isEmpty()
+               && committedById.map.isEmpty()
+               && committedByExecuteAt.map.isEmpty();
+    }
+
+    public void setEmpty()
+    {
+        maxTimestamp.setEmpty();
+        lastExecutedTimestamp.setEmpty();
+        lastExecutedMicros.setEmpty();
+        lastWriteTimestamp.setEmpty();
+        blindWitnessed.setEmpty();
+        uncommitted.map.setEmpty();
+        committedById.map.setEmpty();
+        committedByExecuteAt.map.setEmpty();
+    }
+
+    public AccordCommandsForKey initialize()
+    {
+        maxTimestamp.set(Defaults.maxTimestamp);
+        lastExecutedTimestamp.load(Defaults.lastExecutedTimestamp);
+        lastExecutedMicros.load(Defaults.lastExecutedMicros);
+        lastWriteTimestamp.load(Defaults.lastWriteTimestamp);
+        blindWitnessed.load(new TreeSet<>());
+        uncommitted.map.load(new TreeMap<>());
+        committedById.map.load(new TreeMap<>());
+        committedByExecuteAt.map.load(new TreeMap<>());
+        return this;
+    }
+
+    @Override
+    public boolean hasModifications()
+    {
+        return maxTimestamp.hasModifications()
+               || lastExecutedTimestamp.hasModifications()
+               || lastExecutedMicros.hasModifications()
+               || lastWriteTimestamp.hasModifications()
+               || blindWitnessed.hasModifications()
+               || uncommitted.map.hasModifications()
+               || committedById.map.hasModifications()
+               || committedByExecuteAt.map.hasModifications();
+    }
+
+    @Override
+    public void clearModifiedFlag()
+    {
+        maxTimestamp.clearModifiedFlag();
+        lastExecutedTimestamp.clearModifiedFlag();
+        lastExecutedMicros.clearModifiedFlag();
+        lastWriteTimestamp.clearModifiedFlag();
+        blindWitnessed.clearModifiedFlag();
+        uncommitted.map.clearModifiedFlag();
+        committedById.map.clearModifiedFlag();
+        committedByExecuteAt.map.clearModifiedFlag();
+    }
+
+    @Override
+    public boolean isLoaded()
+    {
+        return maxTimestamp.isLoaded()
+               && lastExecutedTimestamp.isLoaded()
+               && lastExecutedMicros.isLoaded()
+               && lastWriteTimestamp.isLoaded()
+               && blindWitnessed.isLoaded()
+               && uncommitted.map.isLoaded()
+               && committedById.map.isLoaded()
+               && committedByExecuteAt.map.isLoaded();
+    }
+
+    public CommandStore commandStore()
+    {
+        return commandStore;
+    }
+
+    @Override
+    public PartitionKey key()
+    {
+        return key;
+    }
+
+    @Override
+    public long estimatedSizeOnHeap()
+    {
+        long size = EMPTY_SIZE;
+        size += maxTimestamp.estimatedSizeOnHeap(AccordObjectSizes::timestamp);
+        size += lastExecutedTimestamp.estimatedSizeOnHeap(AccordObjectSizes::timestamp);
+        size += lastExecutedMicros.estimatedSizeOnHeap();
+        size += lastWriteTimestamp.estimatedSizeOnHeap(AccordObjectSizes::timestamp);
+        size += blindWitnessed.estimatedSizeOnHeap(AccordObjectSizes::timestamp);
+        size += uncommitted.map.estimatedSizeOnHeap(AccordObjectSizes::timestamp, ByteBufferUtil::estimatedSizeOnHeap);
+        size += committedById.map.estimatedSizeOnHeap(AccordObjectSizes::timestamp, ByteBufferUtil::estimatedSizeOnHeap);
+        size += committedByExecuteAt.map.estimatedSizeOnHeap(AccordObjectSizes::timestamp, ByteBufferUtil::estimatedSizeOnHeap);
+        return size;
+    }
+
+    @Override
+    public CommandTimeseries uncommitted()
+    {
+        return uncommitted;
+    }
+
+    @Override
+    public CommandTimeseries committedById()
+    {
+        return committedById;
+    }
+
+    @Override
+    public CommandTimeseries committedByExecuteAt()
+    {
+        return committedByExecuteAt;
+    }
+
+    @Override
+    public Timestamp max()
+    {
+        return maxTimestamp.get();
+    }
+
+    @Override
+    public void updateMax(Timestamp timestamp)
+    {
+        if (isFullInstance())
+        {
+            if (maxTimestamp.get().compareTo(timestamp) >= 0)
+                return;
+            maxTimestamp.set(timestamp);
+        }
+        else
+        {
+            Preconditions.checkState(isWriteOnlyInstance());
+            blindWitnessed.blindAdd(timestamp);
+        }
+    }
+
+    public void applyBlindWitnessedTimestamps()
+    {
+        if (isEmpty() || blindWitnessed.getView().isEmpty())
+            return;
+
+        logger.trace("Applying blind witnessed timestamps for {}: {}", key(), blindWitnessed.getView());
+        blindWitnessed.getView().forEach(this::updateMax);
+        blindWitnessed.clear();
+    }
+
+    public void updateSummaries(AccordCommand command)
+    {
+        if (command.status.get().hasBeen(Status.Committed))
+        {
+            if (command.status.previous() == null || !command.status.previous().hasBeen(Status.Committed))
+                uncommitted.map.blindRemove(command.txnId());
+
+            ByteBuffer bb = AccordPartialCommand.WithDeps.serializer.serialize(command);
+            committedById.map.blindPut(command.txnId(), bb);
+            committedByExecuteAt.map.blindPut(command.executeAt(), bb);
+        }
+        else
+        {
+            uncommitted.map.blindPut(command.txnId(), AccordPartialCommand.WithDeps.serializer.serialize(command));
+        }
+    }
+
+    private static long getTimestampMicros(Timestamp timestamp)
+    {
+        return timestamp.real + timestamp.logical;
+    }
+
+    private void maybeUpdatelastTimestamp(Timestamp executeAt, boolean isForWriteTxn)
+    {
+        Timestamp lastWrite = lastWriteTimestamp.get();
+
+        if (executeAt.compareTo(lastWrite) < 0)
+            throw new IllegalArgumentException(String.format("%s is less than the most recent write timestamp %s", executeAt, lastWrite));
+
+        Timestamp lastExecuted = lastExecutedTimestamp.get();
+        int cmp = executeAt.compareTo(lastExecuted);
+        // execute can be in the past if it's for a read and after the most recent write
+        if (cmp == 0 || (!isForWriteTxn && cmp < 0))
+            return;
+        if (cmp < 0)
+            throw new IllegalArgumentException(String.format("%s is less than the most recent executed timestamp %s", executeAt, lastExecuted));
+
+        long micros = getTimestampMicros(executeAt);
+        long lastMicros = lastExecutedMicros.get();
+        lastExecutedTimestamp.set(executeAt);
+        lastExecutedMicros.set(Math.max(micros, lastMicros + 1));
+        if (isForWriteTxn)
+            lastWriteTimestamp.set(executeAt);
+    }
+
+    public int nowInSecondsFor(Timestamp executeAt, boolean isForWriteTxn)
+    {
+        maybeUpdatelastTimestamp(executeAt, isForWriteTxn);
+        // we use the executeAt time instead of the monotonic database timestamp to prevent uneven
+        // ttl expiration in extreme cases, ie 1M+ writes/second to a key causing timestamps to overflow
+        // into the next second on some keys and not others.
+        return (int) TimeUnit.MICROSECONDS.toSeconds(getTimestampMicros(lastExecutedTimestamp.get()));
+    }
+
+    public long timestampMicrosFor(Timestamp executeAt, boolean isForWriteTxn)
+    {
+        maybeUpdatelastTimestamp(executeAt, isForWriteTxn);
+        return lastExecutedMicros.get();
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        AccordCommandsForKey that = (AccordCommandsForKey) o;
+        return commandStore == that.commandStore
+               && key.equals(that.key)
+               && maxTimestamp.equals(that.maxTimestamp)
+               && lastExecutedTimestamp.equals(that.lastExecutedTimestamp)
+               && lastExecutedMicros.equals(that.lastExecutedMicros)
+               && lastWriteTimestamp.equals(that.lastWriteTimestamp)
+               && blindWitnessed.equals(that.blindWitnessed)
+               && uncommitted.map.equals(that.uncommitted.map)
+               && committedById.map.equals(that.committedById.map)
+               && committedByExecuteAt.map.equals(that.committedByExecuteAt.map);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash(commandStore, key, blindWitnessed, maxTimestamp, lastExecutedTimestamp, lastExecutedMicros, lastWriteTimestamp, uncommitted, committedById, committedByExecuteAt);
+    }
+
+    @Override
+    public String toString()
+    {
+        return "AccordCommandsForKey{" +
+               "key=" + key +
+               ", maxTs=" + maxTimestamp +
+               ", lastExecutedTimestamp=" + lastExecutedTimestamp +
+               ", lastExecutedMicros=" + lastExecutedMicros +
+               ", lastWriteTimestamp=" + lastWriteTimestamp +
+//               ", blindWitnessed=" + blindWitnessed +
+//               ", uncommitted=" + uncommitted.map +
+//               ", committedById=" + committedById.map +
+//               ", committedByExecuteAt=" + committedByExecuteAt.map +
+               '}';
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordConfigurationService.java b/src/java/org/apache/cassandra/service/accord/AccordConfigurationService.java
new file mode 100644
index 0000000000..784af390eb
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordConfigurationService.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import accord.api.ConfigurationService;
+import accord.local.Node;
+import accord.topology.Topology;
+import org.apache.cassandra.utils.concurrent.Future;
+
+/**
+ * Currently a stubbed out config service meant to be triggered from a dtest
+ */
+public class AccordConfigurationService implements ConfigurationService
+{
+    private final Node.Id localId;
+    private final List<Listener> listeners = new ArrayList<>();
+    private final List<Topology> epochs = new ArrayList<>();
+
+    public AccordConfigurationService(Node.Id localId)
+    {
+        this.localId = localId;
+        epochs.add(Topology.EMPTY);
+    }
+
+    @Override
+    public synchronized void registerListener(Listener listener)
+    {
+        listeners.add(listener);
+    }
+
+    @Override
+    public synchronized Topology currentTopology()
+    {
+        return epochs.get(epochs.size() - 1);
+    }
+
+    @Override
+    public Topology getTopologyForEpoch(long epoch)
+    {
+        return epochs.get((int) epoch);
+    }
+
+    @Override
+    public void fetchTopologyForEpoch(long epoch)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void acknowledgeEpoch(long epoch)
+    {
+        Topology acknowledged = getTopologyForEpoch(epoch);
+        for (Node.Id node : acknowledged.nodes())
+        {
+            if (node.equals(localId))
+                continue;
+            for (Listener listener : listeners)
+                listener.onEpochSyncComplete(node, epoch);
+        }
+    }
+
+    public void createEpochFromConfig()
+    {
+        Topology topology = AccordTopologyUtils.createTopology(epochs.size());
+        epochs.add(topology);
+        for (Listener listener : listeners)
+            listener.onTopologyUpdate(topology);
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordKeyspace.java b/src/java/org/apache/cassandra/service/accord/AccordKeyspace.java
new file mode 100644
index 0000000000..d2c3ef6928
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordKeyspace.java
@@ -0,0 +1,843 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.NavigableSet;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.local.CommandStore;
+import accord.local.Node;
+import accord.local.Status;
+import accord.primitives.Ballot;
+import accord.primitives.Deps;
+import accord.primitives.Timestamp;
+import accord.primitives.TxnId;
+import accord.utils.DeterministicIdentitySet;
+import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.cql3.statements.schema.CreateTableStatement;
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.ClusteringComparator;
+import org.apache.cassandra.db.Columns;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.DeletionTime;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.ReadExecutionController;
+import org.apache.cassandra.db.RegularAndStaticColumns;
+import org.apache.cassandra.db.SinglePartitionReadCommand;
+import org.apache.cassandra.db.Slices;
+import org.apache.cassandra.db.filter.ClusteringIndexFilter;
+import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter;
+import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.db.filter.DataLimits;
+import org.apache.cassandra.db.filter.RowFilter;
+import org.apache.cassandra.db.marshal.ByteBufferAccessor;
+import org.apache.cassandra.db.marshal.BytesType;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.db.marshal.TupleType;
+import org.apache.cassandra.db.marshal.ValueAccessor;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.rows.BTreeRow;
+import org.apache.cassandra.db.rows.Cell;
+import org.apache.cassandra.db.rows.CellPath;
+import org.apache.cassandra.db.rows.ComplexColumnData;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.db.rows.RowIterator;
+import org.apache.cassandra.db.transform.FilteredPartitions;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.Functions;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.schema.TableId;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.schema.Tables;
+import org.apache.cassandra.schema.Types;
+import org.apache.cassandra.schema.Views;
+import org.apache.cassandra.serializers.UUIDSerializer;
+import org.apache.cassandra.service.accord.AccordCommandsForKey.SeriesKind;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.service.accord.db.AccordData;
+import org.apache.cassandra.service.accord.serializers.CommandSerializers;
+import org.apache.cassandra.service.accord.store.StoredNavigableMap;
+import org.apache.cassandra.service.accord.store.StoredSet;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.Clock;
+
+import static java.lang.String.format;
+import static org.apache.cassandra.cql3.QueryProcessor.executeOnceInternal;
+import static org.apache.cassandra.db.rows.BufferCell.*;
+import static org.apache.cassandra.schema.SchemaConstants.ACCORD_KEYSPACE_NAME;
+import static org.apache.cassandra.utils.ByteBufferUtil.EMPTY_BYTE_BUFFER;
+import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
+
+public class AccordKeyspace
+{
+    private static final Logger logger = LoggerFactory.getLogger(AccordKeyspace.class);
+
+    public static final String COMMANDS = "commands";
+    public static final String COMMAND_SERIES = "command_series";
+    public static final String COMMANDS_FOR_KEY = "commands_for_key";
+
+    private static final String TIMESTAMP_TUPLE = "tuple<bigint, bigint, int, bigint>";
+    private static final TupleType TIMESTAMP_TYPE = new TupleType(Lists.newArrayList(LongType.instance, LongType.instance, Int32Type.instance, LongType.instance));
+    private static final String KEY_TUPLE = "tuple<uuid, blob>";
+
+    private static final ClusteringIndexFilter FULL_PARTITION = new ClusteringIndexSliceFilter(Slices.ALL, false);
+
+    // TODO: store timestamps as blobs (confirm there are no negative numbers, or offset)
+    private static final TableMetadata Commands =
+        parse(COMMANDS,
+              "accord commands",
+              "CREATE TABLE %s ("
+              + "store_generation int,"
+              + "store_index int,"
+              + format("txn_id %s,", TIMESTAMP_TUPLE)
+              + "status int,"
+              + "home_key blob,"
+              + "home_key_version int,"
+              + "progress_key blob,"
+              + "progress_key_version blob,"
+              + "is_globally_persistent boolean,"
+              + "txn_version int,"
+              + "txn blob,"
+              + format("execute_at %s,", TIMESTAMP_TUPLE)
+              + format("promised_ballot %s,", TIMESTAMP_TUPLE)
+              + format("accepted_ballot %s,", TIMESTAMP_TUPLE)
+              + "dependencies_version int,"
+              + "dependencies blob,"
+              + "writes_version int,"
+              + "writes blob,"
+              + "result_version int,"
+              + "result blob,"
+              + format("waiting_on_commit map<%s, blob>,", TIMESTAMP_TUPLE)
+              + format("waiting_on_apply map<%s, blob>,", TIMESTAMP_TUPLE)
+              + "listeners set<blob>, "
+              + format("blocking_commit_on set<%s>, ", TIMESTAMP_TUPLE)
+              + format("blocking_apply_on set<%s>, ", TIMESTAMP_TUPLE)
+              + "PRIMARY KEY((store_generation, store_index, txn_id))"
+              + ')');
+
+    private static ColumnMetadata getColumn(TableMetadata metadata, String name)
+    {
+        ColumnMetadata column = metadata.getColumn(new ColumnIdentifier(name, true));
+        if (column == null)
+            throw new IllegalArgumentException(String.format("Unknown column %s for %s.%s", name, metadata.keyspace, metadata.name));
+        return column;
+    }
+
+    private static class CommandsColumns
+    {
+        static final ClusteringComparator keyComparator = Commands.partitionKeyAsClusteringComparator();
+        static final ColumnMetadata status = getColumn(Commands, "status");
+        static final ColumnMetadata home_key = getColumn(Commands, "home_key");
+        static final ColumnMetadata home_key_version = getColumn(Commands, "home_key_version");
+        static final ColumnMetadata progress_key = getColumn(Commands, "progress_key");
+        static final ColumnMetadata progress_key_version = getColumn(Commands, "progress_key_version");
+        static final ColumnMetadata is_globally_persistent = getColumn(Commands, "is_globally_persistent");
+        static final ColumnMetadata txn_version = getColumn(Commands, "txn_version");
+        static final ColumnMetadata txn = getColumn(Commands, "txn");
+        static final ColumnMetadata execute_at = getColumn(Commands, "execute_at");
+        static final ColumnMetadata promised_ballot = getColumn(Commands, "promised_ballot");
+        static final ColumnMetadata accepted_ballot = getColumn(Commands, "accepted_ballot");
+        static final ColumnMetadata dependencies_version = getColumn(Commands, "dependencies_version");
+        static final ColumnMetadata dependencies = getColumn(Commands, "dependencies");
+        static final ColumnMetadata writes_version = getColumn(Commands, "writes_version");
+        static final ColumnMetadata writes = getColumn(Commands, "writes");
+        static final ColumnMetadata result_version = getColumn(Commands, "result_version");
+        static final ColumnMetadata result = getColumn(Commands, "result");
+        static final ColumnMetadata waiting_on_commit = getColumn(Commands, "waiting_on_commit");
+        static final ColumnMetadata waiting_on_apply = getColumn(Commands, "waiting_on_apply");
+        static final ColumnMetadata listeners = getColumn(Commands, "listeners");
+        static final ColumnMetadata blocking_commit_on = getColumn(Commands, "blocking_commit_on");
+        static final ColumnMetadata blocking_apply_on = getColumn(Commands, "blocking_apply_on");
+    }
+
+    private static final TableMetadata CommandsForKey =
+        parse(COMMANDS_FOR_KEY,
+              "accord commands per key",
+              "CREATE TABLE %s ("
+              + "store_generation int, "
+              + "store_index int, "
+              + format("key %s, ", KEY_TUPLE)
+              + format("max_timestamp %s static, ", TIMESTAMP_TUPLE)
+              + format("last_executed_timestamp %s static, ", TIMESTAMP_TUPLE)
+              + format("last_executed_micros bigint static, ")
+              + format("last_write_timestamp %s static, ", TIMESTAMP_TUPLE)
+              + format("blind_witnessed set<%s> static, ", TIMESTAMP_TUPLE)
+              + "series int, "
+              + format("timestamp %s, ", TIMESTAMP_TUPLE)
+              + "data blob, "
+              + "PRIMARY KEY((store_generation, store_index, key), series, timestamp)"
+              + ')');
+
+    private static class CommandsForKeyColumns
+    {
+        static final ClusteringComparator keyComparator = CommandsForKey.partitionKeyAsClusteringComparator();
+        static final ColumnFilter allColumns = ColumnFilter.all(CommandsForKey);
+        static final ColumnMetadata max_timestamp = getColumn(CommandsForKey, "max_timestamp");
+        static final ColumnMetadata last_executed_timestamp = getColumn(CommandsForKey, "last_executed_timestamp");
+        static final ColumnMetadata last_executed_micros = getColumn(CommandsForKey, "last_executed_micros");
+        static final ColumnMetadata last_write_timestamp = getColumn(CommandsForKey, "last_write_timestamp");
+        static final ColumnMetadata blind_witnessed = getColumn(CommandsForKey, "blind_witnessed");
+
+        static final ColumnMetadata series = getColumn(CommandsForKey, "series");
+        static final ColumnMetadata timestamp = getColumn(CommandsForKey, "timestamp");
+        static final ColumnMetadata data = getColumn(CommandsForKey, "data");
+
+        static final Columns statics = Columns.from(Lists.newArrayList(max_timestamp, last_executed_timestamp, last_executed_micros, last_write_timestamp, blind_witnessed));
+        static final Columns regulars = Columns.from(Lists.newArrayList(data));
+        private static final RegularAndStaticColumns all = new RegularAndStaticColumns(statics, regulars);
+        private static final RegularAndStaticColumns justStatic = new RegularAndStaticColumns(statics, Columns.NONE);
+        private static final RegularAndStaticColumns justRegular = new RegularAndStaticColumns(Columns.NONE, regulars);
+
+        static boolean hasStaticChanges(AccordCommandsForKey commandsForKey)
+        {
+            return commandsForKey.maxTimestamp.hasModifications()
+                   || commandsForKey.lastExecutedTimestamp.hasModifications()
+                   || commandsForKey.lastExecutedMicros.hasModifications()
+                   || commandsForKey.blindWitnessed.hasModifications();
+        }
+
+        static RegularAndStaticColumns columnsFor(AccordCommandsForKey commandsForKey)
+        {
+            boolean hasStaticChanges = hasStaticChanges(commandsForKey);
+            boolean hasRegularChanges = commandsForKey.uncommitted.map.hasAdditions()
+                                        || commandsForKey.committedById.map.hasAdditions()
+                                        || commandsForKey.committedByExecuteAt.map.hasAdditions();
+
+            if (hasStaticChanges && hasRegularChanges)
+                return all;
+            else if (hasStaticChanges)
+                return justStatic;
+            else if (hasRegularChanges)
+                return justRegular;
+            else
+                throw new IllegalArgumentException();
+        }
+    }
+
+    private static TableMetadata parse(String name, String description, String cql)
+    {
+        return CreateTableStatement.parse(format(cql, name), ACCORD_KEYSPACE_NAME)
+                                   .id(TableId.forSystemTable(ACCORD_KEYSPACE_NAME, name))
+                                   .comment(description)
+                                   .gcGraceSeconds((int) TimeUnit.DAYS.toSeconds(90))
+                                   .build();
+    }
+
+    public static KeyspaceMetadata metadata()
+    {
+        return KeyspaceMetadata.create(ACCORD_KEYSPACE_NAME, KeyspaceParams.local(), tables(), Views.none(), Types.none(), Functions.none());
+    }
+
+    private static Tables tables()
+    {
+        return Tables.of(Commands, CommandsForKey);
+    }
+
+    private static <T> ByteBuffer serialize(T obj, IVersionedSerializer<T> serializer, int version) throws IOException
+    {
+        int size = (int) serializer.serializedSize(obj, version);
+        try (DataOutputBuffer out = new DataOutputBuffer(size))
+        {
+            serializer.serialize(obj, out, version);
+            assert size == out.buffer().limit();
+            return out.buffer();
+        }
+    }
+
+    private static <T> ByteBuffer serializeOrNull(T obj, IVersionedSerializer<T> serializer, int version) throws IOException
+    {
+        return obj != null ? serialize(obj, serializer, version) : EMPTY_BYTE_BUFFER;
+    }
+
+    private static <T> T deserialize(ByteBuffer bytes, IVersionedSerializer<T> serializer, int version) throws IOException
+    {
+        try (DataInputBuffer in = new DataInputBuffer(bytes, true))
+        {
+            return serializer.deserialize(in, version);
+        }
+    }
+
+    private static <T> T deserializeOrNull(ByteBuffer bytes, IVersionedSerializer<T> serializer, int version) throws IOException
+    {
+        return bytes != null && ! ByteBufferAccessor.instance.isEmpty(bytes) ? deserialize(bytes, serializer, version) : null;
+    }
+
+    private static Map<ByteBuffer, ByteBuffer> serializeWaitingOn(Map<TxnId, ByteBuffer> waitingOn)
+    {
+        Map<ByteBuffer, ByteBuffer> result = Maps.newHashMapWithExpectedSize(waitingOn.size());
+        for (Map.Entry<TxnId, ByteBuffer> entry : waitingOn.entrySet())
+            result.put(serializeTimestamp(entry.getKey()), entry.getValue());
+        return result;
+    }
+
+    private static NavigableMap<TxnId, ByteBuffer> deserializeWaitingOn(Map<ByteBuffer, ByteBuffer> serialized)
+    {
+        if (serialized == null || serialized.isEmpty())
+            return new TreeMap<>();
+
+        NavigableMap<TxnId, ByteBuffer> result = new TreeMap<>();
+        for (Map.Entry<ByteBuffer, ByteBuffer> entry : serialized.entrySet())
+            result.put(deserializeTimestampOrNull(entry.getKey(), TxnId::new), entry.getValue());
+        return result;
+    }
+
+    private static NavigableMap<TxnId, ByteBuffer> deserializeWaitingOn(UntypedResultSet.Row row, String name)
+    {
+        return deserializeWaitingOn(row.getMap(name, BytesType.instance, BytesType.instance));
+    }
+
+    private static <T extends Timestamp, S extends Set<T>> S deserializeTimestampSet(Set<ByteBuffer> serialized, Supplier<S> setFactory, TimestampFactory<T> timestampFactory)
+    {
+        S result = setFactory.get();
+        if (serialized == null || serialized.isEmpty())
+            return result;
+
+        for (ByteBuffer bytes : serialized)
+            result.add(deserializeTimestampOrNull(bytes, timestampFactory));
+
+        return result;
+    }
+
+    private static NavigableSet<TxnId> deserializeBlocking(UntypedResultSet.Row row, String name)
+    {
+        return deserializeTimestampSet(row.getSet(name, BytesType.instance), TreeSet::new, TxnId::new);
+    }
+
+    public static Set<ByteBuffer> serializeListeners(Set<ListenerProxy> listeners)
+    {
+        Set<ByteBuffer> result = Sets.newHashSetWithExpectedSize(listeners.size());
+        for (ListenerProxy listener : listeners)
+        {
+            result.add(listener.identifier());
+        }
+        return result;
+    }
+
+    private static DeterministicIdentitySet<ListenerProxy> deserializeListeners(CommandStore commandStore, Set<ByteBuffer> serialized) throws IOException
+    {
+        if (serialized == null || serialized.isEmpty())
+            return new DeterministicIdentitySet<>();
+        DeterministicIdentitySet<ListenerProxy> result = new DeterministicIdentitySet<>();
+        for (ByteBuffer bytes : serialized)
+        {
+            result.add(ListenerProxy.deserialize(commandStore, bytes, ByteBufferAccessor.instance, 0));
+        }
+        return result;
+    }
+
+    private static DeterministicIdentitySet<ListenerProxy> deserializeListeners(CommandStore commandStore, UntypedResultSet.Row row, String name) throws IOException
+    {
+        return deserializeListeners(commandStore, row.getSet(name, BytesType.instance));
+    }
+
+    private static <K extends Comparable<?>, V> void addStoredMapChanges(Row.Builder builder,
+                                                                         ColumnMetadata column,
+                                                                         long timestamp,
+                                                                         int nowInSec,
+                                                                         StoredNavigableMap<K, V> map,
+                                                                         Function<K, ByteBuffer> serializeKey,
+                                                                         Function<V, ByteBuffer> serializeVal)
+    {
+        if (map.wasCleared())
+        {
+            if (!map.hasAdditions())
+            {
+                builder.addComplexDeletion(column, new DeletionTime(timestamp, nowInSec));
+                return;
+            }
+            else
+                builder.addComplexDeletion(column, new DeletionTime(timestamp - 1, nowInSec));
+        }
+
+        map.forEachAddition((k, v) -> builder.addCell(live(column, timestamp, serializeVal.apply(v), CellPath.create(serializeKey.apply(k)))));
+
+        if (!map.wasCleared())
+            map.forEachDeletion(k -> builder.addCell(tombstone(column, timestamp, nowInSec, CellPath.create(serializeKey.apply(k)))));
+    }
+
+    private static <T extends Comparable<?>> void addStoredSetChanges(Row.Builder builder,
+                                                                      ColumnMetadata column,
+                                                                      long timestamp,
+                                                                      int nowInSec,
+                                                                      StoredSet<T, ?> map,
+                                                                      Function<T, ByteBuffer> serialize)
+    {
+        if (map.wasCleared())
+        {
+            if (!map.hasAdditions())
+            {
+                builder.addComplexDeletion(column, new DeletionTime(timestamp, nowInSec));
+                return;
+            }
+            else
+                builder.addComplexDeletion(column, new DeletionTime(timestamp - 1, nowInSec));
+        }
+
+        map.forEachAddition(i -> builder.addCell(live(column, timestamp, EMPTY_BYTE_BUFFER, CellPath.create(serialize.apply(i)))));
+
+        if (!map.wasCleared())
+            map.forEachDeletion(k -> builder.addCell(tombstone(column, timestamp, nowInSec, CellPath.create(serialize.apply(k)))));
+    }
+
+    public static Mutation getCommandMutation(AccordCommand command, long timestampMicros)
+    {
+        try
+        {
+            Preconditions.checkArgument(command.hasModifications());
+
+            // TODO: convert to byte arrays
+            ValueAccessor<ByteBuffer> accessor = ByteBufferAccessor.instance;
+
+            Row.Builder builder = BTreeRow.unsortedBuilder();
+            builder.newRow(Clustering.EMPTY);
+            int nowInSeconds = (int) TimeUnit.MICROSECONDS.toSeconds(timestampMicros);
+            int version = MessagingService.current_version;
+            ByteBuffer versionBytes = accessor.valueOf(version);
+
+            if (command.status.hasModifications())
+                builder.addCell(live(CommandsColumns.status, timestampMicros, accessor.valueOf(command.status.get().ordinal())));
+
+            if (command.homeKey.hasModifications())
+            {
+                builder.addCell(live(CommandsColumns.home_key_version, timestampMicros, versionBytes));
+                builder.addCell(live(CommandsColumns.home_key, timestampMicros, serializeOrNull((AccordKey) command.homeKey.get(), AccordKey.serializer, version)));
+            }
+
+            if (command.progressKey.hasModifications())
+            {
+                builder.addCell(live(CommandsColumns.progress_key_version, timestampMicros, versionBytes));
+                builder.addCell(live(CommandsColumns.progress_key, timestampMicros, serializeOrNull((AccordKey) command.progressKey.get(), AccordKey.serializer, version)));
+            }
+
+            if (command.isGloballyPersistent.hasModifications())
+                builder.addCell(live(CommandsColumns.is_globally_persistent, timestampMicros, accessor.valueOf(command.isGloballyPersistent.get())));
+
+            if (command.txn.hasModifications())
+            {
+                builder.addCell(live(CommandsColumns.txn_version, timestampMicros, versionBytes));
+                builder.addCell(live(CommandsColumns.txn, timestampMicros, serializeOrNull(command.txn.get(), CommandSerializers.txn, version)));
+            }
+
+            if (command.executeAt.hasModifications())
+                builder.addCell(live(CommandsColumns.execute_at, timestampMicros, serializeTimestamp(command.executeAt.get())));
+
+            if (command.promised.hasModifications())
+                builder.addCell(live(CommandsColumns.promised_ballot, timestampMicros, serializeTimestamp(command.promised.get())));
+
+            if (command.accepted.hasModifications())
+                builder.addCell(live(CommandsColumns.accepted_ballot, timestampMicros, serializeTimestamp(command.accepted.get())));
+
+            if (command.deps.hasModifications())
+            {
+                builder.addCell(live(CommandsColumns.dependencies_version, timestampMicros, versionBytes));
+                builder.addCell(live(CommandsColumns.dependencies, timestampMicros, serialize(command.deps.get(), CommandSerializers.deps, version)));
+            }
+
+            if (command.writes.hasModifications())
+            {
+                builder.addCell(live(CommandsColumns.writes_version, timestampMicros, versionBytes));
+                builder.addCell(live(CommandsColumns.writes, timestampMicros, serialize(command.writes.get(), CommandSerializers.writes, version)));
+            }
+
+            if (command.result.hasModifications())
+            {
+                builder.addCell(live(CommandsColumns.result_version, timestampMicros, versionBytes));
+                builder.addCell(live(CommandsColumns.result, timestampMicros, serialize((AccordData) command.result.get(), AccordData.serializer, version)));
+            }
+
+            if (command.waitingOnCommit.hasModifications())
+            {
+                addStoredMapChanges(builder, CommandsColumns.waiting_on_commit,
+                                    timestampMicros, nowInSeconds, command.waitingOnCommit,
+                                    AccordKeyspace::serializeTimestamp, bytes -> bytes);
+            }
+
+            if (command.blockingCommitOn.hasModifications())
+            {
+                addStoredSetChanges(builder, CommandsColumns.blocking_commit_on,
+                                    timestampMicros, nowInSeconds, command.blockingApplyOn,
+                                    AccordKeyspace::serializeTimestamp);
+            }
+
+            if (command.waitingOnApply.hasModifications())
+            {
+                addStoredMapChanges(builder, CommandsColumns.waiting_on_apply,
+                                    timestampMicros, nowInSeconds, command.waitingOnApply,
+                                    AccordKeyspace::serializeTimestamp, bytes -> bytes);
+            }
+
+            if (command.blockingApplyOn.hasModifications())
+            {
+                addStoredSetChanges(builder, CommandsColumns.blocking_apply_on,
+                                    timestampMicros, nowInSeconds, command.blockingApplyOn,
+                                    AccordKeyspace::serializeTimestamp);
+            }
+
+            if (command.storedListeners.hasModifications())
+            {
+                addStoredSetChanges(builder, CommandsColumns.listeners,
+                                    timestampMicros, nowInSeconds, command.storedListeners,
+                                    ListenerProxy::identifier);
+            }
+            ByteBuffer key = CommandsColumns.keyComparator.make(command.commandStore().generation(),
+                                                                command.commandStore().index(),
+                                                                serializeTimestamp(command.txnId())).serializeAsPartitionKey();
+            PartitionUpdate update = PartitionUpdate.singleRowUpdate(Commands, key, builder.build());
+            return new Mutation(update);
+        }
+        catch (IOException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static ByteBuffer serializeKey(PartitionKey key)
+    {
+        UUIDSerializer.instance.serialize(key.tableId().asUUID());
+        return TupleType.buildValue(new ByteBuffer[]{UUIDSerializer.instance.serialize(key.tableId().asUUID()),
+                                                     key.partitionKey().getKey()});
+    }
+
+    private static ByteBuffer serializeTimestamp(Timestamp timestamp)
+    {
+        return TupleType.buildValue(new ByteBuffer[]{bytes(timestamp.epoch), bytes(timestamp.real), bytes(timestamp.logical), bytes(timestamp.node.id)});
+    }
+
+    public interface TimestampFactory<T extends Timestamp>
+    {
+        T create(long epoch, long real, int logical, Node.Id node);
+    }
+
+    public static <T extends Timestamp> T deserializeTimestampOrNull(ByteBuffer bytes, TimestampFactory<T> factory)
+    {
+        if (bytes == null || ByteBufferAccessor.instance.isEmpty(bytes))
+            return null;
+        ByteBuffer[] split = TIMESTAMP_TYPE.split(ByteBufferAccessor.instance, bytes);
+        return factory.create(split[0].getLong(), split[1].getLong(), split[2].getInt(), new Node.Id(split[3].getLong()));
+    }
+
+    private static <T extends Timestamp> T deserializeTimestampOrNull(UntypedResultSet.Row row, String name, TimestampFactory<T> factory)
+    {
+        return deserializeTimestampOrNull(row.getBlob(name), factory);
+    }
+
+    public static AccordCommand loadCommand(AccordCommandStore commandStore, TxnId txnId)
+    {
+        AccordCommand command = new AccordCommand(commandStore, txnId);
+        loadCommand(command);
+        return command;
+    }
+
+    private static <T> T deserializeWithVersionOr(UntypedResultSet.Row row, String dataColumn, String versionColumn, IVersionedSerializer<T> serializer, Supplier<T> defaultSupplier) throws IOException
+    {
+        if (!row.has(versionColumn))
+            return defaultSupplier.get();
+
+        return deserialize(row.getBlob(dataColumn), serializer, row.getInt(versionColumn));
+    }
+
+    public static UntypedResultSet loadCommandRow(CommandStore commandStore, TxnId txnId)
+    {
+        String cql = "SELECT * FROM %s.%s " +
+                     "WHERE store_generation=? " +
+                     "AND store_index=? " +
+                     "AND txn_id=(?, ?, ?, ?)";
+
+        return executeOnceInternal(String.format(cql, ACCORD_KEYSPACE_NAME, COMMANDS),
+                                   commandStore.generation(),
+                                   commandStore.index(),
+                                   txnId.epoch, txnId.real, txnId.logical, txnId.node.id);
+    }
+
+    public static void loadCommand(AccordCommand command)
+    {
+        Preconditions.checkArgument(!command.isLoaded());
+        TxnId txnId = command.txnId();
+        AccordCommandStore commandStore = command.commandStore();
+        commandStore.checkNotInStoreThread();
+
+        String cql = "SELECT * FROM %s.%s " +
+                     "WHERE store_generation=? " +
+                     "AND store_index=? " +
+                     "AND txn_id=(?, ?, ?, ?)";
+
+        UntypedResultSet result = loadCommandRow(commandStore, command.txnId());
+
+        if (result.isEmpty())
+        {
+            command.setEmpty();
+            return;
+        }
+
+        try
+        {
+            UntypedResultSet.Row row = result.one();
+            Preconditions.checkState(deserializeTimestampOrNull(row, "txn_id", TxnId::new).equals(txnId));
+            command.status.load(Status.values()[row.getInt("status")]);
+            command.homeKey.load(deserializeOrNull(row.getBlob("home_key"), AccordKey.serializer, row.getInt("home_key_version")));
+            command.progressKey.load(deserializeOrNull(row.getBlob("progress_key"), AccordKey.serializer, row.getInt("progress_key_version")));
+            command.isGloballyPersistent.load(row.getBoolean("is_globally_persistent"));
+            command.txn.load(deserializeOrNull(row.getBlob("txn"), CommandSerializers.txn, row.getInt("txn_version")));
+            command.executeAt.load(deserializeTimestampOrNull(row, "execute_at", Timestamp::new));
+            command.promised.load(deserializeTimestampOrNull(row, "promised_ballot", Ballot::new));
+            command.accepted.load(deserializeTimestampOrNull(row, "accepted_ballot", Ballot::new));
+            command.deps.load(deserializeWithVersionOr(row, "dependencies", "dependencies_version", CommandSerializers.deps, () -> Deps.NONE));
+            command.writes.load(deserializeWithVersionOr(row, "writes", "writes_version", CommandSerializers.writes, () -> null));
+            command.result.load(deserializeWithVersionOr(row, "result", "result_version", AccordData.serializer, () -> null));
+            command.waitingOnCommit.load(deserializeWaitingOn(row, "waiting_on_commit"));
+            command.blockingCommitOn.load(deserializeBlocking(row, "blocking_commit_on"));
+            command.waitingOnApply.load(deserializeWaitingOn(row, "waiting_on_apply"));
+            command.blockingApplyOn.load(deserializeBlocking(row, "blocking_apply_on"));
+            command.storedListeners.load(deserializeListeners(commandStore, row, "listeners"));
+        }
+        catch (IOException e)
+        {
+            logger.error("Exception loading AccordCommand " + command.txnId(), e);
+            throw new RuntimeException(e);
+        }
+        catch (Throwable t)
+        {
+            logger.error("Exception loading AccordCommand " + command.txnId(), t);
+            throw t;
+        }
+    }
+
+    private static void addSeriesMutations(AccordCommandsForKey.Series series,
+                                           PartitionUpdate.Builder partitionBuilder,
+                                           Row.Builder rowBuilder,
+                                           long timestampMicros,
+                                           int nowInSeconds)
+    {
+        if (!series.map.hasModifications())
+            return;
+
+        Row.Deletion deletion = series.map.hasDeletions() ?
+                                Row.Deletion.regular(new DeletionTime(timestampMicros, nowInSeconds)) :
+                                null;
+        ByteBuffer ordinalBytes = bytes(series.kind.ordinal());
+        series.map.forEachAddition((timestamp, bytes) -> {
+            rowBuilder.newRow(Clustering.make(ordinalBytes, serializeTimestamp(timestamp)));
+            rowBuilder.addCell(live(CommandsForKeyColumns.data, timestampMicros, bytes));
+            partitionBuilder.add(rowBuilder.build());
+        });
+        series.map.forEachDeletion(timestamp -> {
+            rowBuilder.newRow(Clustering.make(ordinalBytes, serializeTimestamp(timestamp)));
+            rowBuilder.addRowDeletion(deletion);
+            partitionBuilder.add(rowBuilder.build());
+        });
+    }
+
+    private static DecoratedKey makeKey(CommandStore commandStore, PartitionKey key)
+    {
+        ByteBuffer pk = CommandsForKeyColumns.keyComparator.make(commandStore.generation(),
+                                                                  commandStore.index(),
+                                                                  serializeKey(key)).serializeAsPartitionKey();
+        return CommandsForKey.partitioner.decorateKey(pk);
+    }
+
+    private static DecoratedKey makeKey(AccordCommandsForKey cfk)
+    {
+        return makeKey(cfk.commandStore(), cfk.key());
+    }
+
+    public static Mutation getCommandsForKeyMutation(AccordCommandsForKey cfk, long timestampMicros)
+    {
+        Preconditions.checkArgument(cfk.hasModifications());
+
+        int nowInSeconds = (int) TimeUnit.MICROSECONDS.toSeconds(timestampMicros);
+
+        int expectedRows = (CommandsForKeyColumns.hasStaticChanges(cfk) ? 1 : 0)
+                           + cfk.uncommitted.map.totalModifications()
+                           + cfk.committedById.map.totalModifications()
+                           + cfk.committedByExecuteAt.map.totalModifications();
+
+        PartitionUpdate.Builder partitionBuilder = new PartitionUpdate.Builder(CommandsForKey,
+                                                                               makeKey(cfk),
+                                                                               CommandsForKeyColumns.columnsFor(cfk),
+                                                                               expectedRows);
+
+        Row.Builder rowBuilder = BTreeRow.unsortedBuilder();
+        boolean updateStaticRow = cfk.maxTimestamp.hasModifications()
+                                  || cfk.lastExecutedTimestamp.hasModifications()
+                                  || cfk.lastExecutedMicros.hasModifications()
+                                  || cfk.blindWitnessed.hasModifications();
+        if (updateStaticRow)
+            rowBuilder.newRow(Clustering.STATIC_CLUSTERING);
+
+        if (cfk.maxTimestamp.hasModifications())
+            rowBuilder.addCell(live(CommandsForKeyColumns.max_timestamp, timestampMicros, serializeTimestamp(cfk.maxTimestamp.get())));
+
+        if (cfk.lastExecutedTimestamp.hasModifications())
+            rowBuilder.addCell(live(CommandsForKeyColumns.last_executed_timestamp, timestampMicros, serializeTimestamp(cfk.lastExecutedTimestamp.get())));
+
+        if (cfk.lastExecutedMicros.hasModifications())
+            rowBuilder.addCell(live(CommandsForKeyColumns.last_executed_micros, timestampMicros, ByteBufferUtil.bytes(cfk.lastExecutedMicros.get())));
+
+        if (cfk.lastWriteTimestamp.hasModifications())
+            rowBuilder.addCell(live(CommandsForKeyColumns.last_write_timestamp, timestampMicros, serializeTimestamp(cfk.lastWriteTimestamp.get())));
+
+        if (cfk.blindWitnessed.hasModifications())
+            addStoredSetChanges(rowBuilder, CommandsForKeyColumns.blind_witnessed,
+                                timestampMicros, nowInSeconds, cfk.blindWitnessed,
+                                AccordKeyspace::serializeTimestamp);
+
+        if (updateStaticRow)
+            partitionBuilder.add(rowBuilder.build());
+
+        addSeriesMutations(cfk.uncommitted, partitionBuilder, rowBuilder, timestampMicros, nowInSeconds);
+        addSeriesMutations(cfk.committedById, partitionBuilder, rowBuilder, timestampMicros, nowInSeconds);
+        addSeriesMutations(cfk.committedByExecuteAt, partitionBuilder, rowBuilder, timestampMicros, nowInSeconds);
+
+        return new Mutation(partitionBuilder.build());
+    }
+
+    public static AccordCommandsForKey loadCommandsForKey(AccordCommandStore commandStore, PartitionKey key)
+    {
+        AccordCommandsForKey commandsForKey = new AccordCommandsForKey(commandStore, key);
+        loadCommandsForKey(commandsForKey);
+        return commandsForKey;
+    }
+
+    private static <T> ByteBuffer cellValue(Cell<T> cell)
+    {
+        return cell.accessor().toBuffer(cell.value());
+    }
+
+    // TODO: convert to byte array
+    private static ByteBuffer cellValue(Row row, ColumnMetadata column)
+    {
+        Cell<?> cell = row.getCell(column);
+        return  (cell != null && !cell.isTombstone()) ? cellValue(cell) : null;
+    }
+
+    private static <T> ByteBuffer clusteringValue(Clustering<T> clustering, int idx)
+    {
+        return clustering.accessor().toBuffer(clustering.get(idx));
+    }
+
+    public static SinglePartitionReadCommand getCommandsForKeyRead(CommandStore commandStore, PartitionKey key, int nowInSeconds)
+    {
+        return SinglePartitionReadCommand.create(CommandsForKey, nowInSeconds,
+                                                 CommandsForKeyColumns.allColumns,
+                                                 RowFilter.NONE,
+                                                 DataLimits.NONE,
+                                                 makeKey(commandStore, key),
+                                                 FULL_PARTITION);
+    }
+
+    public static void loadCommandsForKey(AccordCommandsForKey cfk)
+    {
+        Preconditions.checkArgument(!cfk.isLoaded());
+        ((AccordCommandStore) cfk.commandStore()).checkNotInStoreThread();
+        long timestampMicros = TimeUnit.MILLISECONDS.toMicros(Clock.Global.currentTimeMillis());
+        int nowInSeconds = (int) TimeUnit.MICROSECONDS.toSeconds(timestampMicros);
+
+        SinglePartitionReadCommand command = getCommandsForKeyRead(cfk.commandStore(), cfk.key(), nowInSeconds);
+
+        EnumMap<SeriesKind, TreeMap<Timestamp, ByteBuffer>> seriesMaps = new EnumMap<>(SeriesKind.class);
+        for (SeriesKind kind : SeriesKind.values())
+            seriesMaps.put(kind, new TreeMap<>());
+
+        try(ReadExecutionController controller = command.executionController();
+            FilteredPartitions partitions = FilteredPartitions.filter(command.executeLocally(controller), nowInSeconds))
+        {
+            if (!partitions.hasNext())
+            {
+                cfk.setEmpty();
+                return;
+            }
+
+            try (RowIterator partition = partitions.next())
+            {
+                // empty static row will be interpreted as all null cells which will cause everything to be initialized
+                Row staticRow = partition.staticRow();
+                Cell<?> cell = staticRow.getCell(CommandsForKeyColumns.max_timestamp);
+                cfk.maxTimestamp.load(cell != null && !cell.isTombstone() ? deserializeTimestampOrNull(cellValue(cell), Timestamp::new)
+                                                                          : AccordCommandsForKey.Defaults.maxTimestamp);
+
+                cell = staticRow.getCell(CommandsForKeyColumns.last_executed_timestamp);
+                cfk.lastExecutedTimestamp.load(cell != null && !cell.isTombstone() ? deserializeTimestampOrNull(cellValue(cell), Timestamp::new)
+                                                                                   : AccordCommandsForKey.Defaults.lastExecutedTimestamp);
+
+                cell = staticRow.getCell(CommandsForKeyColumns.last_executed_micros);
+                ByteBuffer microsBytes = cell != null && !cell.isTombstone() ? cellValue(cell) : null;
+                cfk.lastExecutedMicros.load(microsBytes != null ? microsBytes.getLong(microsBytes.position())
+                                                                : AccordCommandsForKey.Defaults.lastExecutedMicros);
+
+                cell = staticRow.getCell(CommandsForKeyColumns.last_write_timestamp);
+                cfk.lastWriteTimestamp.load(cell != null && !cell.isTombstone() ? deserializeTimestampOrNull(cellValue(cell), Timestamp::new)
+                                                                                   : AccordCommandsForKey.Defaults.lastWriteTimestamp);
+
+                TreeSet<Timestamp> blindWitnessed = new TreeSet<>();
+                ComplexColumnData cmplx = staticRow.getComplexColumnData(CommandsForKeyColumns.blind_witnessed);
+                if (cmplx != null)
+                    cmplx.forEach(c -> blindWitnessed.add(deserializeTimestampOrNull(c.path().get(0), Timestamp::new)));
+                cfk.blindWitnessed.load(blindWitnessed);
+
+                while (partition.hasNext())
+                {
+                    Row row = partition.next();
+                    Clustering<?> clustering = row.clustering();
+                    int ordinal = Int32Type.instance.compose(clusteringValue(clustering, 0));
+                    Timestamp timestamp = deserializeTimestampOrNull(clusteringValue(clustering, 1), Timestamp::new);
+                    ByteBuffer data = cellValue(row, CommandsForKeyColumns.data);
+                    if (data == null)
+                        continue;
+                    seriesMaps.get(SeriesKind.values()[ordinal]).put(timestamp, data);
+                }
+            }
+            Preconditions.checkState(!partitions.hasNext());
+
+            cfk.uncommitted.map.load(seriesMaps.get(SeriesKind.UNCOMMITTED));
+            cfk.committedById.map.load(seriesMaps.get(SeriesKind.COMMITTED_BY_ID));
+            cfk.committedByExecuteAt.map.load(seriesMaps.get(SeriesKind.COMMITTED_BY_EXECUTE_AT));
+        }
+        catch (Throwable t)
+        {
+            logger.error("Exception loading AccordCommandsForKey " + cfk.key(), t);
+            throw t;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordMessageSink.java b/src/java/org/apache/cassandra/service/accord/AccordMessageSink.java
new file mode 100644
index 0000000000..b723b27e6e
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordMessageSink.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.util.EnumMap;
+import java.util.Map;
+
+import com.google.common.base.Preconditions;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.api.MessageSink;
+import accord.local.Node;
+import accord.messages.Callback;
+import accord.messages.MessageType;
+import accord.messages.Reply;
+import accord.messages.ReplyContext;
+import accord.messages.Request;
+import org.apache.cassandra.locator.InetAddressAndPort;
+import org.apache.cassandra.net.Message;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.net.Verb;
+
+import static org.apache.cassandra.service.accord.EndpointMapping.getEndpoint;
+
+public class AccordMessageSink implements MessageSink
+{
+    private static final Logger logger = LoggerFactory.getLogger(AccordMessageSink.class);
+
+    private static class VerbMapping
+    {
+        private static final VerbMapping instance = new VerbMapping();
+
+        private final Map<MessageType, Verb> mapping = new EnumMap<>(MessageType.class);
+
+        private VerbMapping()
+        {
+            mapping.put(MessageType.PREACCEPT_REQ,          Verb.ACCORD_PREACCEPT_REQ);
+            mapping.put(MessageType.PREACCEPT_RSP,          Verb.ACCORD_PREACCEPT_RSP);
+            mapping.put(MessageType.ACCEPT_REQ,             Verb.ACCORD_ACCEPT_REQ);
+            mapping.put(MessageType.ACCEPT_RSP,             Verb.ACCORD_ACCEPT_RSP);
+            mapping.put(MessageType.ACCEPT_INVALIDATE_REQ,  Verb.ACCORD_ACCEPT_INVALIDATE_REQ);
+            mapping.put(MessageType.COMMIT_REQ,             Verb.ACCORD_COMMIT_REQ);
+            mapping.put(MessageType.COMMIT_INVALIDATE,      Verb.ACCORD_COMMIT_INVALIDATE_REQ);
+            mapping.put(MessageType.APPLY_REQ,              Verb.ACCORD_APPLY_REQ);
+            mapping.put(MessageType.APPLY_RSP,              Verb.ACCORD_APPLY_RSP);
+            mapping.put(MessageType.APPLY_AND_CHECK_REQ,    Verb.ACCORD_APPLY_AND_CHECK_REQ);
+            mapping.put(MessageType.APPLY_AND_CHECK_RSP,    Verb.ACCORD_APPLY_AND_CHECK_RSP);
+            mapping.put(MessageType.READ_REQ,               Verb.ACCORD_READ_REQ);
+            mapping.put(MessageType.READ_RSP,               Verb.ACCORD_READ_RSP);
+            mapping.put(MessageType.BEGIN_RECOVER_REQ,      Verb.ACCORD_RECOVER_REQ);
+            mapping.put(MessageType.BEGIN_RECOVER_RSP,      Verb.ACCORD_RECOVER_RSP);
+            mapping.put(MessageType.BEGIN_INVALIDATE_REQ,   Verb.ACCORD_BEGIN_INVALIDATE_REQ);
+            mapping.put(MessageType.BEGIN_INVALIDATE_RSP,   Verb.ACCORD_BEGIN_INVALIDATE_RSP);
+            mapping.put(MessageType.WAIT_ON_COMMIT_REQ,     Verb.ACCORD_WAIT_COMMIT_REQ);
+            mapping.put(MessageType.WAIT_ON_COMMIT_RSP,     Verb.ACCORD_WAIT_COMMIT_RSP);
+            mapping.put(MessageType.INFORM_REQ,             Verb.ACCORD_INFORM_OF_TXN_REQ);
+            mapping.put(MessageType.INFORM_RSP,             Verb.ACCORD_INFORM_OF_TXN_RSP);
+            mapping.put(MessageType.INFORM_PERSISTED_REQ,   Verb.ACCORD_INFORM_OF_PERSIETENCE_REQ);
+            mapping.put(MessageType.INFORM_PERSISTED_RSP,   Verb.ACCORD_INFORM_OF_PERSISTENCE_RSP);
+            mapping.put(MessageType.CHECK_STATUS_REQ,       Verb.ACCORD_CHECK_STATUS_REQ);
+            mapping.put(MessageType.CHECK_STATUS_RSP,       Verb.ACCORD_CHECK_STATUS_RSP);
+        }
+    }
+
+    private static Verb getVerb(MessageType type)
+    {
+        return VerbMapping.instance.mapping.get(type);
+    }
+
+    @Override
+    public void send(Node.Id to, Request request)
+    {
+        Verb verb = getVerb(request.type());
+        Preconditions.checkArgument(verb != null);
+        Message<Request> message = Message.out(verb, request);
+        InetAddressAndPort endpoint = getEndpoint(to);
+        logger.debug("Sending {} {} to {}", verb, message.payload, endpoint);
+        MessagingService.instance().send(message, endpoint);
+    }
+
+    @Override
+    public void send(Node.Id to, Request request, Callback callback)
+    {
+        Verb verb = getVerb(request.type());
+        Preconditions.checkArgument(verb != null);
+        Message<Request> message = Message.out(verb, request);
+        InetAddressAndPort endpoint = getEndpoint(to);
+        logger.debug("Sending {} {} to {}", verb, message.payload, endpoint);
+        MessagingService.instance().sendWithCallback(message, endpoint, new AccordCallback<>((Callback<Reply>) callback));
+    }
+
+    @Override
+    public void reply(Node.Id replyingToNode, ReplyContext replyContext, Reply reply)
+    {
+        Message<?> replyTo = (Message<?>) replyContext;
+        Message<?> replyMsg = replyTo.responseWith(reply);
+        Preconditions.checkArgument(replyMsg.verb() == getVerb(reply.type()));
+        InetAddressAndPort endpoint = getEndpoint(replyingToNode);
+        logger.debug("Replying {} {} to {}", replyMsg.verb(), replyMsg.payload, endpoint);
+        MessagingService.instance().send(replyMsg, endpoint);
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordObjectSizes.java b/src/java/org/apache/cassandra/service/accord/AccordObjectSizes.java
new file mode 100644
index 0000000000..e4ea7049b7
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordObjectSizes.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.util.Map;
+
+import accord.api.Key;
+import accord.local.Node;
+import accord.primitives.Deps;
+import accord.primitives.Keys;
+import accord.primitives.Timestamp;
+import accord.primitives.TxnId;
+import accord.txn.Txn;
+import accord.txn.Writes;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.db.AccordQuery;
+import org.apache.cassandra.service.accord.db.AccordRead;
+import org.apache.cassandra.service.accord.db.AccordUpdate;
+import org.apache.cassandra.service.accord.db.AccordWrite;
+import org.apache.cassandra.utils.ObjectSizes;
+
+public class AccordObjectSizes
+{
+    public static long key(Key key)
+    {
+        return ((AccordKey.PartitionKey) key).estimatedSizeOnHeap();
+    }
+
+    private static final long EMPTY_KEYS_SIZE = ObjectSizes.measure(Keys.of());
+    public static long keys(Keys keys)
+    {
+        long size = EMPTY_KEYS_SIZE;
+        size += ObjectSizes.sizeOfReferenceArray(keys.size());
+        for (int i=0, mi=keys.size(); i<mi; i++)
+            size += key(keys.get(i));
+        return size;
+    }
+
+    private static final long EMPTY_TXN = ObjectSizes.measure(new Txn.InMemory(null, null, null));
+    public static long txn(Txn txn)
+    {
+        long size = EMPTY_TXN;
+        size += keys(txn.keys());
+        size += ((AccordRead) txn.read()).estimatedSizeOnHeap();
+        if (txn.update() != null)
+            size += ((AccordUpdate) txn.update()).estimatedSizeOnHeap();
+        size += ((AccordQuery) txn.query()).estimatedSizeOnHeap();
+        return size;
+    }
+
+    private static final long TIMESTAMP_SIZE = ObjectSizes.measureDeep(new Timestamp(0, 0, 0, new Node.Id(0)));
+
+    public static long timestamp()
+    {
+        return TIMESTAMP_SIZE;
+    }
+    public static long timestamp(Timestamp timestamp)
+    {
+        return TIMESTAMP_SIZE;
+    }
+
+    private static final long EMPTY_DEPS_SIZE = ObjectSizes.measureDeep(Deps.NONE);
+    public static long dependencies(Deps dependencies)
+    {
+        long size = EMPTY_DEPS_SIZE;
+        for (Map.Entry<Key, TxnId> entry : dependencies)
+        {
+            size += key(entry.getKey());
+            size += timestamp(entry.getValue());
+        }
+        return size;
+    }
+
+    private static final long EMPTY_WRITES_SIZE = ObjectSizes.measure(new Writes(null, null, null));
+    public static long writes(Writes writes)
+    {
+        long size = EMPTY_WRITES_SIZE;
+        size += timestamp(writes.executeAt);
+        size += keys(writes.keys);
+        if (writes.write != null)
+            size += ((AccordWrite) writes.write).estimatedSizeOnHeap();
+        return size;
+    }
+
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordPartialCommand.java b/src/java/org/apache/cassandra/service/accord/AccordPartialCommand.java
new file mode 100644
index 0000000000..1419a7595f
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordPartialCommand.java
@@ -0,0 +1,338 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+import accord.local.Listener;
+import accord.local.PartialCommand;
+import accord.local.Status;
+import accord.primitives.Deps;
+import accord.primitives.Timestamp;
+import accord.primitives.TxnId;
+import accord.txn.Txn;
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.service.accord.async.AsyncContext;
+import org.apache.cassandra.service.accord.serializers.CommandSerializers;
+
+import static org.apache.cassandra.service.accord.serializers.NullableSerializer.deserializeNullable;
+import static org.apache.cassandra.service.accord.serializers.NullableSerializer.serializeNullable;
+import static org.apache.cassandra.service.accord.serializers.NullableSerializer.serializedSizeNullable;
+
+public class AccordPartialCommand implements PartialCommand
+{
+    public static final PartialCommandSerializer<PartialCommand> serializer = new PartialCommandSerializer<PartialCommand>()
+    {
+        @Override
+        public PartialCommand getCachedFull(TxnId txnId, AsyncContext context)
+        {
+            return context.commands.get(txnId);
+        }
+
+        @Override
+        public void addToContext(PartialCommand command, AsyncContext context)
+        {
+            context.commands.addPartialCommand((AccordPartialCommand) command);
+        }
+
+        @Override
+        public PartialCommand deserializeBody(TxnId txnId, Txn txn, Timestamp executeAt, Status status, DataInputPlus in, Version version) throws IOException
+        {
+            return new AccordPartialCommand(txnId, txn, executeAt, status);
+        }
+    };
+
+    private final TxnId txnId;
+    private final Txn txn;
+    private final Timestamp executeAt;
+    private final Status status;
+    private List<Listener> removedListeners = null;
+
+    public AccordPartialCommand(TxnId txnId, Txn txn, Timestamp executeAt, Status status)
+    {
+        this.txnId = txnId;
+        this.txn = txn;
+        this.executeAt = executeAt;
+        this.status = status;
+    }
+
+    @Override
+    public TxnId txnId()
+    {
+        return txnId;
+    }
+
+    @Override
+    public Txn txn()
+    {
+        return txn;
+    }
+
+    @Override
+    public Timestamp executeAt()
+    {
+        return executeAt;
+    }
+
+    @Override
+    public Status status()
+    {
+        return status;
+    }
+
+    @Override
+    public void removeListener(Listener listener)
+    {
+        if (removedListeners == null)
+            removedListeners = new ArrayList<>();
+        removedListeners.add(listener);
+    }
+
+    public boolean hasRemovedListeners()
+    {
+        return removedListeners != null && !removedListeners.isEmpty();
+    }
+
+    public void forEachRemovedListener(Consumer<Listener> consumer)
+    {
+        removedListeners.forEach(consumer);
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        AccordPartialCommand that = (AccordPartialCommand) o;
+        return Objects.equals(txnId, that.txnId) && Objects.equals(txn, that.txn) && Objects.equals(executeAt, that.executeAt) && status == that.status && Objects.equals(removedListeners, that.removedListeners);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash(txnId, txn, executeAt, status, removedListeners);
+    }
+
+    @Override
+    public String toString()
+    {
+        return "AccordPartialCommand{" +
+               "txnId=" + txnId +
+               ", txn=" + txn +
+               ", executeAt=" + executeAt +
+               ", status=" + status +
+               ", removedListeners=" + removedListeners +
+               '}';
+    }
+
+    public static class WithDeps extends AccordPartialCommand implements PartialCommand.WithDeps
+    {
+        public static final PartialCommandSerializer<PartialCommand.WithDeps> serializer = new PartialCommandSerializer<PartialCommand.WithDeps>()
+        {
+            @Override
+            public PartialCommand.WithDeps getCachedFull(TxnId txnId, AsyncContext context)
+            {
+                return context.commands.get(txnId);
+            }
+
+            @Override
+            public void addToContext(PartialCommand.WithDeps command, AsyncContext context)
+            {
+                context.commands.addPartialCommand((AccordPartialCommand) command);
+            }
+
+            @Override
+            public PartialCommand.WithDeps deserializeBody(TxnId txnId, Txn txn, Timestamp executeAt, Status status, DataInputPlus in, Version version) throws IOException
+            {
+                Deps deps = deserializeNullable(in, version.msg_version, CommandSerializers.deps);
+                return new AccordPartialCommand.WithDeps(txnId, txn, executeAt, status, deps);
+            }
+
+            @Override
+            public void serialize(PartialCommand.WithDeps command, DataOutputPlus out, Version version) throws IOException
+            {
+                super.serialize(command, out, version);
+                serializeNullable(command.savedDeps(), out, version.msg_version, CommandSerializers.deps);
+            }
+
+            @Override
+            public int serializedSize(PartialCommand.WithDeps command, Version version)
+            {
+                int size = super.serializedSize(command, version) ;
+                size += serializedSizeNullable(command.savedDeps(), version.msg_version, CommandSerializers.deps);
+                return size;
+            }
+
+            @Override
+            public boolean needsUpdate(AccordCommand command)
+            {
+                return super.needsUpdate(command) || command.deps.hasModifications();
+            }
+        };
+
+        private final Deps deps;
+
+        public WithDeps(TxnId txnId, Txn txn, Timestamp executeAt, Status status, Deps deps)
+        {
+            super(txnId, txn, executeAt, status);
+            this.deps = deps;
+        }
+
+        @Override
+        public Deps savedDeps()
+        {
+            return deps;
+        }
+
+        @Override
+        public boolean equals(Object o)
+        {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            AccordPartialCommand.WithDeps withDeps = (AccordPartialCommand.WithDeps) o;
+            return super.equals(o) && Objects.equals(deps, withDeps.deps);
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return Objects.hash(super.hashCode(), deps);
+        }
+    }
+
+    public static abstract class PartialCommandSerializer<T extends PartialCommand>
+    {
+        public enum Version
+        {
+            VERSION_0(0, MessagingService.current_version);
+            final byte version;
+            final int msg_version;
+
+            Version(int version, int msg_version)
+            {
+                this.version = (byte) version;
+                this.msg_version = msg_version;
+            }
+
+            public static final Version current = VERSION_0;
+
+            public static Version fromByte(byte b)
+            {
+                switch (b)
+                {
+                    case 0:
+                        return VERSION_0;
+                    default:
+                        throw new IllegalArgumentException();
+                }
+            }
+        }
+
+        public void serialize(T command, DataOutputPlus out, Version version) throws IOException
+        {
+            out.write(version.version);
+            CommandSerializers.txnId.serialize(command.txnId(), out, version.msg_version);
+            CommandSerializers.status.serialize(command.status(), out, version.msg_version);
+            serializeNullable(command.txn(), out, version.msg_version, CommandSerializers.txn);
+            serializeNullable(command.executeAt(), out, version.msg_version, CommandSerializers.timestamp);
+        }
+
+        public ByteBuffer serialize(T command)
+        {
+            Version version = Version.current;
+            int size = serializedSize(command, version);
+            try (DataOutputBuffer out = new DataOutputBuffer(size))
+            {
+                serialize(command, out, version);
+                return out.buffer(false);
+            }
+            catch (IOException e)
+            {
+                throw new RuntimeException(e);
+            }
+        }
+
+        public Version deserializeVersion(DataInputPlus in) throws IOException
+        {
+            return Version.fromByte(in.readByte());
+        }
+
+        // check for cached command first, otherwise deserialize
+        public T deserialize(AccordCommandStore commandStore, DataInputPlus in) throws IOException
+        {
+            Version version = deserializeVersion(in);
+            TxnId txnId = CommandSerializers.txnId.deserialize(in, version.msg_version);
+            AsyncContext context = commandStore.getContext();
+            T command = getCachedFull(txnId, context);
+            if (command != null)
+                return command;
+
+            Status status = CommandSerializers.status.deserialize(in, version.msg_version);
+            Txn txn = deserializeNullable(in, version.msg_version, CommandSerializers.txn);
+            Timestamp executeAt = deserializeNullable(in, version.msg_version, CommandSerializers.timestamp);
+            T partial = deserializeBody(txnId, txn, executeAt, status, in, version);
+            addToContext(partial, context);
+            return partial;
+        }
+
+        public T deserialize(AccordCommandStore commandStore, ByteBuffer bytes)
+        {
+            try (DataInputBuffer in = new DataInputBuffer(bytes, true))
+            {
+                return deserialize(commandStore, in);
+            }
+            catch (IOException e)
+            {
+                throw new RuntimeException(e);
+            }
+        }
+
+        public int serializedSize(T command, Version version)
+        {
+            int size = TypeSizes.sizeof(version.version);
+            size += CommandSerializers.txnId.serializedSize();
+            size += CommandSerializers.status.serializedSize(command.status(), version.msg_version);
+            size += serializedSizeNullable(command.txn(), version.msg_version, CommandSerializers.txn);
+            size += serializedSizeNullable(command.executeAt(), version.msg_version, CommandSerializers.timestamp);
+            return size;
+        }
+
+        public abstract T getCachedFull(TxnId txnId, AsyncContext context);
+        public abstract void addToContext(T command, AsyncContext context);
+        public abstract T deserializeBody(TxnId txnId, Txn txn, Timestamp executeAt, Status status, DataInputPlus in, Version version) throws IOException;
+
+        /**
+         * Determines if current modifications require updating command data duplicated elsewhere
+         */
+        public boolean needsUpdate(AccordCommand command)
+        {
+            return command.txn.hasModifications() || command.executeAt.hasModifications() || command.status.hasModifications();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/service/accord/AccordService.java b/src/java/org/apache/cassandra/service/accord/AccordService.java
new file mode 100644
index 0000000000..02c62510b6
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordService.java
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.util.Arrays;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import accord.impl.SimpleProgressLog;
+import accord.local.Node;
+import accord.messages.Reply;
+import accord.messages.Request;
+import org.apache.cassandra.concurrent.Shutdownable;
+import org.apache.cassandra.net.IVerbHandler;
+import org.apache.cassandra.service.accord.api.AccordAgent;
+import org.apache.cassandra.service.accord.api.AccordScheduler;
+import org.apache.cassandra.utils.Clock;
+import org.apache.cassandra.utils.ExecutorUtils;
+import org.apache.cassandra.utils.FBUtilities;
+
+public class AccordService implements Shutdownable
+{
+    public final Node node;
+    private final Shutdownable nodeShutdown;
+    private final AccordMessageSink messageSink;
+    public final AccordConfigurationService configService;
+    private final AccordScheduler scheduler;
+    private final AccordVerbHandler verbHandler;
+
+    private static class Handle
+    {
+        public static final AccordService instance = new AccordService();
+    }
+
+    public static AccordService instance()
+    {
+        return Handle.instance;
+    }
+
+    public static long uniqueNow()
+    {
+        return TimeUnit.MILLISECONDS.toMicros(Clock.Global.currentTimeMillis());
+    }
+
+    private AccordService()
+    {
+        Node.Id localId = EndpointMapping.endpointToId(FBUtilities.getBroadcastAddressAndPort());
+        this.messageSink = new AccordMessageSink();
+        this.configService = new AccordConfigurationService(localId);
+        this.scheduler = new AccordScheduler();
+        this.node = new Node(localId,
+                             messageSink,
+                             configService,
+                             AccordService::uniqueNow,
+                             () -> null,
+                             new AccordAgent(),
+                             new Random(),
+                             scheduler,
+                             SimpleProgressLog::new,
+                             AccordCommandStores::new);
+        this.nodeShutdown = toShutdownable(node);
+        this.verbHandler = new AccordVerbHandler(this.node);
+    }
+
+    public <T extends Request> IVerbHandler<T> verbHandler()
+    {
+        return verbHandler;
+    }
+
+    @VisibleForTesting
+    public void createEpochFromConfigUnsafe()
+    {
+        configService.createEpochFromConfig();
+    }
+
+    public static long nowInMicros()
+    {
+        return TimeUnit.MILLISECONDS.toMicros(Clock.Global.currentTimeMillis());
+    }
+
+    @VisibleForTesting
+    AccordMessageSink messageSink()
+    {
+        return messageSink;
+    }
+
+    public void setCacheSize(long kb)
+    {
+        long bytes = kb << 10;
+        AccordCommandStores commandStores = (AccordCommandStores) node.commandStores();
+        commandStores.setCacheSize(bytes);
+    }
+
+    @Override
+    public boolean isTerminated()
+    {
+        return scheduler.isTerminated();
+    }
+
+    @Override
+    public void shutdown()
+    {
+        ExecutorUtils.shutdown(Arrays.asList(scheduler, nodeShutdown));
+    }
+
+    @Override
+    public Object shutdownNow()
+    {
+        ExecutorUtils.shutdownNow(Arrays.asList(scheduler, nodeShutdown));
+        return null;
+    }
+
+    @Override
+    public boolean awaitTermination(long timeout, TimeUnit units) throws InterruptedException
+    {
+        try
+        {
+            ExecutorUtils.awaitTermination(timeout, units, Arrays.asList(scheduler, nodeShutdown));
+            return true;
+        }
+        catch (TimeoutException e)
+        {
+            return false;
+        }
+    }
+
+    @VisibleForTesting
+    public void shutdownAndWait(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        ExecutorUtils.shutdownAndWait(timeout, unit, this);
+    }
+
+    private static Shutdownable toShutdownable(Node node)
+    {
+        return new Shutdownable() {
+            private volatile boolean isShutdown = false;
+
+            @Override
+            public boolean isTerminated()
+            {
+                // we don't know about terminiated... so settle for shutdown!
+                return isShutdown;
+            }
+
+            @Override
+            public void shutdown()
+            {
+                isShutdown = true;
+                node.shutdown();
+            }
+
+            @Override
+            public Object shutdownNow()
+            {
+                // node doesn't offer shutdownNow
+                shutdown();
+                return null;
+            }
+
+            @Override
+            public boolean awaitTermination(long timeout, TimeUnit units) throws InterruptedException
+            {
+                // node doesn't offer
+                return true;
+            }
+        };
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordState.java b/src/java/org/apache/cassandra/service/accord/AccordState.java
new file mode 100644
index 0000000000..ba755fb1eb
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordState.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.util.function.Function;
+
+import org.apache.cassandra.service.accord.store.StoredNavigableMap;
+import org.apache.cassandra.service.accord.store.StoredSet;
+import org.apache.cassandra.utils.concurrent.Future;
+
+public interface AccordState<K>
+{
+    public enum Kind { FULL, WRITE_ONLY, READ_ONLY }
+
+    K key();
+
+    boolean hasModifications();
+
+    void clearModifiedFlag();
+
+    boolean isEmpty();
+
+    boolean isLoaded();
+
+    long estimatedSizeOnHeap();
+
+    default Kind kind()
+    {
+        return Kind.FULL;
+    }
+
+    default boolean isFullInstance()
+    {
+        return kind() == Kind.FULL;
+    }
+
+    default boolean isWriteOnlyInstance()
+    {
+        return kind() == Kind.WRITE_ONLY;
+    }
+
+    default boolean isReadOnlyInstance()
+    {
+        return kind() == Kind.READ_ONLY;
+    }
+
+    interface WriteOnly<K, V extends AccordState<K>> extends AccordState<K>
+    {
+        @Override
+        default Kind kind()
+        {
+            return Kind.WRITE_ONLY;
+        }
+
+        void future(Future<?> future);
+
+        Future<?> future();
+
+        /**
+         * Apply the write only changes to the full instance
+         */
+        void applyChanges(V instance);
+
+        static <T, K extends Comparable<?>, V> void applyMapChanges(T from, T to, Function<T, StoredNavigableMap<K, V>> getMap)
+        {
+            StoredNavigableMap<K, V> fromMap = getMap.apply(from);
+
+            if (!fromMap.hasModifications())
+                return;
+
+            StoredNavigableMap<K, V> toMap = getMap.apply(to);
+            fromMap.forEachAddition(toMap::blindPut);
+            fromMap.forEachDeletion(toMap::blindRemove);
+        }
+
+        static <T, V extends Comparable<?>> void applySetChanges(T from, T to, Function<T, StoredSet<V, ?>> getSet)
+        {
+            StoredSet<V, ?> fromSet = getSet.apply(from);
+
+            if (!fromSet.hasModifications())
+                return;
+
+            StoredSet<V, ?> toSet = getSet.apply(to);
+            fromSet.forEachAddition(toSet::blindAdd);
+            fromSet.forEachDeletion(toSet::blindRemove);
+        }
+    }
+
+    interface ReadOnly<K, V extends AccordState<K>> extends AccordState<K>
+    {
+        @Override
+        default Kind kind()
+        {
+            return Kind.READ_ONLY;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordStateCache.java b/src/java/org/apache/cassandra/service/accord/AccordStateCache.java
new file mode 100644
index 0000000000..0a04a6e550
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordStateCache.java
@@ -0,0 +1,646 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.txn.Txn;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.concurrent.Future;
+import org.apache.cassandra.utils.concurrent.FutureCombiner;
+
+/**
+ * Cache for AccordCommand and AccordCommandsForKey, available memory is shared between the two object types.
+ *
+ * Supports dynamic object sizes. After each acquire/free cycle, the cacheable objects size is recomputed to
+ * account for data added/removed during txn processing if it's modified flag is set
+ *
+ * TODO: explain how items move to and from the active pool and are evicted
+ */
+public class AccordStateCache
+{
+    private static final Logger logger = LoggerFactory.getLogger(AccordStateCache.class);
+
+    private static class WriteOnlyGroup<K, V extends AccordState<K>>
+    {
+        private boolean locked = false;
+        private List<AccordState.WriteOnly<K, V>> items = new ArrayList<>();
+
+        @Override
+        public String toString()
+        {
+            return "WriteOnlyGroup{" +
+                   "locked=" + locked +
+                   ", items=" + items +
+                   '}';
+        }
+
+        void lock()
+        {
+            locked = true;
+        }
+
+        void add(AccordState.WriteOnly<K, V> item)
+        {
+            items.add(item);
+        }
+
+        void purge()
+        {
+            if (locked)
+                return;
+
+            while (!items.isEmpty())
+            {
+                AccordState.WriteOnly<K, V> item = items.get(0);
+
+                // we can't remove items out of order, so if we encounter a write is still pending, we stop
+                if (item.future() == null || !item.future().isDone())
+                    break;
+
+                items.remove(0);
+            }
+        }
+
+        boolean isEmpty()
+        {
+            return items.isEmpty();
+        }
+    }
+
+    static class Node<K, V extends AccordState<K>>
+    {
+        static final long EMPTY_SIZE = ObjectSizes.measure(new AccordStateCache.Node<>(null));
+
+        final V value;
+        private Node<?, ?> prev;
+        private Node<?, ?> next;
+        private int references = 0;
+        private long lastQueriedSize = 0;
+
+        Node(V value)
+        {
+            this.value = value;
+        }
+
+        long size()
+        {
+            long result = EMPTY_SIZE + value.estimatedSizeOnHeap();
+            lastQueriedSize = result;
+            return result;
+        }
+
+        long sizeDelta()
+        {
+            long prevSize = lastQueriedSize;
+            return size() - prevSize;
+        }
+
+        K key()
+        {
+            return value.key();
+        }
+    }
+
+    static class Stats
+    {
+        private long queries;
+        private long hits;
+        private long misses;
+    }
+
+    private static class NamedMap<K, V> extends HashMap<K, V>
+    {
+        final String name;
+
+        public NamedMap(String name)
+        {
+            this.name = name;
+        }
+    }
+
+    public final Map<Object, Node<?, ?>> active = new HashMap<>();
+    private final Map<Object, Node<?, ?>> cache = new HashMap<>();
+    private final Map<Object, WriteOnlyGroup<?, ?>> pendingWriteOnly = new HashMap<>();
+    private final Set<Instance<?, ?>> instances = new HashSet<>();
+
+    private final NamedMap<Object, Future<?>> loadFutures = new NamedMap<>("loadFutures");
+    private final NamedMap<Object, Future<?>> saveFutures = new NamedMap<>("saveFutures");
+
+    private final NamedMap<Object, ReadFuture> readFutures = new NamedMap<>("readFutures");
+    private final NamedMap<Object, Future<?>> writeFutures = new NamedMap<>("writeFutures");
+
+    Node<?, ?> head;
+    Node<?, ?> tail;
+    private long maxSizeInBytes;
+    private long bytesCached = 0;
+    private final Stats stats = new Stats();
+
+    public AccordStateCache(long maxSizeInBytes)
+    {
+        this.maxSizeInBytes = maxSizeInBytes;
+    }
+
+    public void setMaxSize(long size)
+    {
+        maxSizeInBytes = size;
+        maybeEvict();
+    }
+
+    private void unlink(Node<?, ?> node)
+    {
+        Node<?, ?> prev = node.prev;
+        Node<?, ?> next = node.next;
+
+        if (prev == null)
+        {
+            Preconditions.checkState(head == node);
+            head = next;
+        }
+        else
+        {
+            prev.next = next;
+        }
+
+        if (next == null)
+        {
+            Preconditions.checkState(tail == node);
+            tail = prev;
+        }
+        else
+        {
+            next.prev = prev;
+        }
+
+        node.prev = null;
+        node.next = null;
+    }
+
+    private void push(Node<?, ?> node)
+    {
+        if (head != null)
+        {
+            node.prev = null;
+            node.next = head;
+            head.prev = node;
+            head = node;
+        }
+        else
+        {
+            head = node;
+            tail = node;
+        }
+    }
+
+    private void updateSize(Node<?, ?> node)
+    {
+        bytesCached += node.sizeDelta();
+    }
+
+    // don't evict if there's an outstanding save future. If an item is evicted then reloaded
+    // before it's mutation is applied, out of date info will be loaded
+    private boolean canEvict(Object key)
+    {
+        Future<?> future = getFuture(saveFutures, key);
+        return future == null || future.isDone();
+    }
+
+    private void maybeEvict()
+    {
+        if (bytesCached <= maxSizeInBytes)
+            return;
+
+        Node<?, ?> current = tail;
+        while (current != null && bytesCached > maxSizeInBytes)
+        {
+            Node<?, ?> evict = current;
+            current = current.prev;
+
+            // if there are any dangling write only groups, apply them and
+            // move their futures into write futures so we don't evict
+            applyAndRemoveWriteOnlyGroup(evict.value);
+            if (!canEvict(evict.key()))
+                continue;
+
+            logger.trace("Evicting {} {}", evict.value.getClass().getSimpleName(), evict.key());
+            unlink(evict);
+            cache.remove(evict.key());
+            bytesCached -= evict.size();
+        }
+    }
+
+    private static <K, F extends Future<?>> F getFuture(NamedMap<Object, F> futuresMap, K key)
+    {
+        F r = futuresMap.get(key);
+        if (r == null)
+            return null;
+
+        if (!r.isDone())
+            return r;
+
+        if (logger.isTraceEnabled())
+            logger.trace("Clearing future for {} from {}: {}", key, futuresMap.name, r);
+        futuresMap.remove(key);
+        return null;
+    }
+
+    private static <K, F extends Future<?>> void setFuture(Map<Object, F> futuresMap, K key, F future)
+    {
+        Preconditions.checkState(!futuresMap.containsKey(key));
+        futuresMap.put(key, future);
+    }
+
+    private static <K> void mergeFuture(Map<Object, Future<?>> futuresMap, K key, Future<?> future)
+    {
+        Future<?> existing = futuresMap.get(key);
+        if (existing != null && !existing.isDone())
+        {
+            logger.trace("Merging future {} with existing {}", future, existing);
+            future = FutureCombiner.allOf(ImmutableList.of(existing, future));
+        }
+
+        futuresMap.put(key, future);
+    }
+
+    private <K> void maybeClearFuture(K key)
+    {
+        // will clear if it's done
+        getFuture(loadFutures, key);
+        getFuture(saveFutures, key);
+        getFuture(readFutures, key);
+        getFuture(writeFutures, key);
+    }
+
+    public <K, V extends AccordState<K>> void applyAndRemoveWriteOnlyGroup(V instance)
+    {
+        WriteOnlyGroup<K, V> group = (WriteOnlyGroup<K, V>) pendingWriteOnly.remove(instance.key());
+        if (group == null)
+            return;
+
+        logger.trace("Applying and removing write only group for {} ({})", instance.key(), group);
+        for (AccordState.WriteOnly<K, V> writeOnly : group.items)
+        {
+            writeOnly.applyChanges(instance);
+            if (!writeOnly.future().isDone())
+                mergeFuture(saveFutures, instance.key(), writeOnly.future());
+        }
+    }
+
+    public class Instance<K, V extends AccordState<K>>
+    {
+        private final Class<K> keyClass;
+        private final Class<V> valClass;
+        private final Function<K, V> factory;
+        private final Stats stats = new Stats();
+
+        public Instance(Class<K> keyClass, Class<V> valClass, Function<K, V> factory)
+        {
+            this.keyClass = keyClass;
+            this.valClass = valClass;
+            this.factory = factory;
+        }
+
+        @Override
+        public boolean equals(Object o)
+        {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            Instance<?, ?> instance = (Instance<?, ?>) o;
+            return keyClass.equals(instance.keyClass) && valClass.equals(instance.valClass);
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return Objects.hash(keyClass, valClass);
+        }
+
+        private V getOrCreate(K key, boolean createIfAbsent)
+        {
+            stats.queries++;
+            AccordStateCache.this.stats.queries++;
+
+            Node<K, V> node = (Node<K, V>) active.get(key);
+            if (node != null)
+            {
+                stats.hits++;
+                AccordStateCache.this.stats.hits++;
+                node.references++;
+                return node.value;
+            }
+
+            node = (Node<K, V>) cache.remove(key);
+
+            if (node == null)
+            {
+                stats.misses++;
+                AccordStateCache.this.stats.misses++;
+                if (!createIfAbsent)
+                    return null;
+                V value = factory.apply(key);
+                node = new Node<>(value);
+                updateSize(node);
+            }
+            else
+            {
+                stats.hits++;
+                AccordStateCache.this.stats.hits++;
+                unlink(node);
+            }
+
+            Preconditions.checkState(node.references == 0);
+            maybeEvict();
+
+            node.references++;
+            active.put(key, node);
+
+            return node.value;
+        }
+
+        public V getOrCreate(K key)
+        {
+            return getOrCreate(key, true);
+        }
+
+        public V getOrNull(K key)
+        {
+            return getOrCreate(key, false);
+        }
+
+        public void release(V value)
+        {
+            K key = value.key();
+            logger.trace("Releasing resources for {}: {}", key, value);
+            maybeClearFuture(key);
+            Node<K, V> node = (Node<K, V>) active.get(key);
+            Preconditions.checkState(node != null && node.references > 0);
+            Preconditions.checkState(node.value == value);
+            if (--node.references == 0)
+            {
+                logger.trace("Moving {} from active pool to cache", key);
+                active.remove(key);
+                cache.put(key, node);
+                push(node);
+            }
+
+            if (value.hasModifications())
+            {
+                value.clearModifiedFlag();
+                updateSize(node);
+            }
+            maybeEvict();
+        }
+
+        @VisibleForTesting
+        boolean canEvict(K key)
+        {
+            return AccordStateCache.this.canEvict(key);
+        }
+
+        @VisibleForTesting
+        boolean writeOnlyGroupIsLocked(K key)
+        {
+            WriteOnlyGroup<K, V> group = (WriteOnlyGroup<K, V>) pendingWriteOnly.get(key);
+            return group != null && group.locked;
+        }
+
+        @VisibleForTesting
+        int pendingWriteOnlyOperations(K key)
+        {
+            WriteOnlyGroup<K, V> group = (WriteOnlyGroup<K, V>) pendingWriteOnly.get(key);
+            return group != null ? group.items.size() : 0;
+        }
+
+        public void lockWriteOnlyGroupIfExists(K key)
+        {
+            WriteOnlyGroup<K, V> group = (WriteOnlyGroup<K, V>) pendingWriteOnly.get(key);
+            if (group == null)
+                return;
+
+            logger.trace("Locking write only group for {} ({})", key, group);
+            group.purge();
+            if (!group.isEmpty())
+                group.lock();
+        }
+
+        public void applyAndRemoveWriteOnlyGroup(V instance)
+        {
+            AccordStateCache.this.applyAndRemoveWriteOnlyGroup(instance);
+        }
+
+        public void addWriteOnly(AccordState.WriteOnly<K, V> writeOnly)
+        {
+            K key = writeOnly.key();
+            Preconditions.checkArgument(writeOnly.future() != null);
+            WriteOnlyGroup<K, V> group = (WriteOnlyGroup<K, V>) pendingWriteOnly.computeIfAbsent(key, k -> new WriteOnlyGroup<>());
+
+            // if a load future exists for the key we're creating a write group for, we need to lock
+            // the group so the loading instance gets changes applied when it finishes loading
+            if (getLoadFuture(key) != null)
+                group.lock();
+
+            group.add(writeOnly);
+        }
+
+        public void purgeWriteOnly(K key)
+        {
+            WriteOnlyGroup<?, ?> items = pendingWriteOnly.get(key);
+            if (items == null)
+                return;
+
+            items.purge();
+            if (items.isEmpty())
+                pendingWriteOnly.remove(key);
+        }
+
+        public boolean writeOnlyGroupExists(K key)
+        {
+            return pendingWriteOnly.get(key) != null;
+        }
+
+        public int getWriteOnlyGroupSize(K key)
+        {
+            WriteOnlyGroup<?, ?> group = pendingWriteOnly.get(key);
+            return group != null ? group.items.size() : 0;
+        }
+
+        public Future<?> getLoadFuture(K key)
+        {
+            return getFuture(loadFutures, key);
+        }
+
+        public void cleanupLoadFuture(K key)
+        {
+            getLoadFuture(key);
+        }
+
+        @VisibleForTesting
+        public boolean hasLoadFuture(K key)
+        {
+            return loadFutures.get(key) != null;
+        }
+
+        public void setLoadFuture(K key, Future<?> future)
+        {
+            setFuture(loadFutures, key, future);
+        }
+
+        public Future<?> getSaveFuture(K key)
+        {
+            return getFuture(saveFutures, key);
+        }
+
+        public void addSaveFuture(K key, Future<?> future)
+        {
+            logger.trace("Adding save future for {}: {}", key, future);
+            mergeFuture(saveFutures, key, future);
+        }
+
+        public void cleanupSaveFuture(K key)
+        {
+            getSaveFuture(key);
+        }
+
+        @VisibleForTesting
+        public boolean hasSaveFuture(K key)
+        {
+            return saveFutures.get(key) != null;
+        }
+
+        public ReadFuture getReadFuture(K key)
+        {
+            return getFuture(readFutures, key);
+        }
+
+        public void setReadFuture(K key, ReadFuture future)
+        {
+            setFuture(readFutures, key, future);
+        }
+
+        public void cleanupReadFuture(K key)
+        {
+            getReadFuture(key);
+        }
+
+        public Future<Void> getWriteFuture(K key)
+        {
+            return (Future<Void>) getFuture(writeFutures, key);
+        }
+
+        public void setWriteFuture(K key, Future<Void> future)
+        {
+            setFuture(writeFutures, key, future);
+        }
+
+        public void cleanupWriteFuture(K key)
+        {
+            getWriteFuture(key);
+        }
+
+        public long cacheQueries()
+        {
+            return stats.queries;
+        }
+
+        public long cacheHits()
+        {
+            return stats.hits;
+        }
+
+        public long cacheMisses()
+        {
+            return stats.misses;
+        }
+    }
+
+    public <K, V extends AccordState<K>> Instance<K, V> instance(Class<K> keyClass, Class<V> valClass, Function<K, V> factory)
+    {
+        Instance<K, V> instance = new Instance<>(keyClass, valClass, factory);
+        if (!instances.add(instance))
+            throw new IllegalArgumentException(String.format("Cache instances for types %s -> %s already exists",
+                                                             keyClass.getName(), valClass.getName()));
+        return instance;
+    }
+
+    @VisibleForTesting
+    int numActiveEntries()
+    {
+        return active.size();
+    }
+
+    @VisibleForTesting
+    int numCachedEntries()
+    {
+        return cache.size();
+    }
+
+    @VisibleForTesting
+    long bytesCached()
+    {
+        return bytesCached;
+    }
+
+    @VisibleForTesting
+    boolean keyIsActive(Object key)
+    {
+        return active.containsKey(key);
+    }
+
+    @VisibleForTesting
+    boolean keyIsCached(Object key)
+    {
+        return cache.containsKey(key);
+    }
+
+    @VisibleForTesting
+    int references(Object key)
+    {
+        Node<?, ?> node = active.get(key);
+        return node != null ? node.references : 0;
+    }
+
+    public long cacheQueries()
+    {
+        return stats.queries;
+    }
+
+    public long cacheHits()
+    {
+        return stats.hits;
+    }
+
+    public long cacheMisses()
+    {
+        return stats.misses;
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordTopologyUtils.java b/src/java/org/apache/cassandra/service/accord/AccordTopologyUtils.java
new file mode 100644
index 0000000000..332a51379a
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordTopologyUtils.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Preconditions;
+
+import accord.topology.Shard;
+import accord.topology.Topology;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.locator.AbstractReplicationStrategy;
+import org.apache.cassandra.locator.EndpointsForToken;
+import org.apache.cassandra.locator.TokenMetadata;
+import org.apache.cassandra.schema.Schema;
+import org.apache.cassandra.schema.SchemaConstants;
+import org.apache.cassandra.schema.TableId;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.service.accord.api.AccordKey.SentinelKey;
+import org.apache.cassandra.service.accord.api.AccordKey.TokenKey;
+
+public class AccordTopologyUtils
+{
+    private static Shard createShard(TokenRange range, EndpointsForToken natural, EndpointsForToken pending)
+    {
+        return new Shard(range,
+                         natural.stream().map(EndpointMapping::getId).collect(Collectors.toList()),
+                         natural.stream().map(EndpointMapping::getId).collect(Collectors.toSet()),
+                         pending.stream().map(EndpointMapping::getId).collect(Collectors.toSet()));
+    }
+
+    private static TokenRange minRange(TableId tableId, Token token)
+    {
+        return new TokenRange(SentinelKey.min(tableId), TokenKey.max(tableId, token));
+    }
+
+    private static TokenRange maxRange(TableId tableId, Token token)
+    {
+        return new TokenRange(TokenKey.max(tableId, token), SentinelKey.max(tableId));
+    }
+
+    private static TokenRange range(TableId tableId, Token left, Token right)
+    {
+        return new TokenRange(TokenKey.max(tableId, left), TokenKey.max(tableId, right));
+    }
+
+    public static List<Shard> createShards(TableMetadata tableMetadata, TokenMetadata tokenMetadata)
+    {
+        TableId tableId = tableMetadata.id;
+        String keyspace = tableMetadata.keyspace;
+
+        AbstractReplicationStrategy replication = Keyspace.open(keyspace).getReplicationStrategy();
+        Set<Token> tokenSet = new HashSet<>(tokenMetadata.sortedTokens());
+        tokenSet.addAll(tokenMetadata.getBootstrapTokens().keySet());
+        tokenMetadata.getMovingEndpoints().forEach(p -> tokenSet.add(p.left));
+        List<Token> tokens = new ArrayList<>(tokenSet);
+        tokens.sort(Comparator.naturalOrder());
+
+        List<Shard> shards = new ArrayList<>(tokens.size() + 1);
+        Shard finalShard = null;
+        for (int i=0, mi=tokens.size(); i<mi; i++)
+        {
+            Token token = tokens.get(i);
+            EndpointsForToken natural = replication.getNaturalReplicasForToken(token);
+            EndpointsForToken pending = tokenMetadata.pendingEndpointsForToken(token, keyspace);
+            if (i == 0)
+            {
+                shards.add(createShard(minRange(tableId, token), natural, pending));
+                finalShard = createShard(maxRange(tableId, tokens.get(mi-1)), natural, pending);
+            }
+            else
+            {
+                Token prev = tokens.get(i - 1);
+                shards.add(createShard(range(tableId, prev, token), natural, pending));
+            }
+        }
+        shards.add(finalShard);
+
+        return shards;
+    }
+
+    public static Topology createTopology(long epoch)
+    {
+        List<TableId> tableIds = new ArrayList<>();
+        TokenMetadata tokenMetadata = StorageService.instance.getTokenMetadata();
+        for (String ksname: Schema.instance.getKeyspaces())
+        {
+            // TODO: add a table metadata flag to enable and enforce accord use
+            if (SchemaConstants.LOCAL_SYSTEM_KEYSPACE_NAMES.contains(ksname))
+                continue;
+            if (SchemaConstants.REPLICATED_SYSTEM_KEYSPACE_NAMES.contains(ksname))
+                continue;
+
+            Keyspace keyspace = Keyspace.open(ksname);
+            for (TableMetadata tableMetadata : keyspace.getMetadata().tables)
+            {
+                tableIds.add(tableMetadata.id);
+            }
+        }
+
+        tableIds.sort(Comparator.naturalOrder());
+
+        List<Shard> shards = new ArrayList<>();
+        for (TableId tableId : tableIds)
+        {
+            TableMetadata tableMetadata = Schema.instance.getTableMetadata(tableId);
+            Preconditions.checkNotNull(tableMetadata);
+            shards.addAll(createShards(tableMetadata, tokenMetadata));
+        }
+
+        return new Topology(epoch, shards.toArray(new Shard[0]));
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/AccordTxnBuilder.java b/src/java/org/apache/cassandra/service/accord/AccordTxnBuilder.java
new file mode 100644
index 0000000000..1eb8b3915f
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/AccordTxnBuilder.java
@@ -0,0 +1,209 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Preconditions;
+
+import accord.api.Key;
+import accord.primitives.Keys;
+import accord.txn.Txn;
+import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.cql3.QueryHandler;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.cql3.statements.ModificationStatement;
+import org.apache.cassandra.cql3.statements.SelectStatement;
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.IMutation;
+import org.apache.cassandra.db.ReadQuery;
+import org.apache.cassandra.db.SinglePartitionReadCommand;
+import org.apache.cassandra.db.SinglePartitionReadQuery;
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.Schema;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.db.AccordQuery;
+import org.apache.cassandra.service.accord.db.AccordRead;
+import org.apache.cassandra.service.accord.db.AccordUpdate;
+import org.apache.cassandra.service.accord.db.AccordUpdate.UpdatePredicate;
+import org.apache.cassandra.service.accord.db.AccordUpdate.UpdatePredicate.Type;
+
+public class AccordTxnBuilder
+{
+    private Set<AccordKey.PartitionKey> keys = new HashSet<>();
+    private List<SinglePartitionReadCommand> reads = new ArrayList<>();
+    private AccordQuery query = AccordQuery.ALL;
+    private List<AccordUpdate.AbstractUpdate> updates = new ArrayList<>();
+    private List<UpdatePredicate> predicates = new ArrayList<>();
+
+    public AccordTxnBuilder withRead(String query, Object... values)
+    {
+        QueryHandler.Prepared prepared = QueryProcessor.prepareInternal(query);
+        Preconditions.checkArgument(prepared.statement instanceof SelectStatement);
+
+        SelectStatement select = (SelectStatement)prepared.statement;
+        ReadQuery readQuery = select.getQuery(QueryProcessor.makeInternalOptions(prepared.statement, values), 0);
+        SinglePartitionReadQuery.Group<SinglePartitionReadCommand> selectQuery = (SinglePartitionReadQuery.Group<SinglePartitionReadCommand>) readQuery;
+        for (SinglePartitionReadCommand command : selectQuery.queries)
+        {
+            keys.add(AccordKey.of(command));
+            reads.add(command);
+        }
+        return this;
+    }
+
+    public AccordTxnBuilder withWrite(String query, Object... values)
+    {
+        QueryHandler.Prepared prepared = QueryProcessor.prepareInternal(query);
+        Preconditions.checkArgument(prepared.statement instanceof ModificationStatement);
+        ModificationStatement modification = (ModificationStatement) prepared.statement;
+        // TODO: look into getting partition updates directly
+        Preconditions.checkArgument(!modification.requiresRead());
+        List<? extends IMutation> mutations = modification.getMutations(ClientState.forInternalCalls(), QueryProcessor.makeInternalOptions(prepared.statement, values), false, 0, 0, 0);
+        for (IMutation mutation : mutations)
+        {
+            for (PartitionUpdate update : mutation.getPartitionUpdates())
+            {
+                keys.add(new AccordKey.PartitionKey(update.metadata().id, update.partitionKey()));
+                updates.add(new AccordUpdate.SimpleUpdate(update));
+            }
+        }
+        return this;
+    }
+
+    public AccordTxnBuilder withAppend(String keyspace, String table, Object key, Map<String, String> appends)
+    {
+        TableMetadata metadata = Schema.instance.getTableMetadata(keyspace, table);
+        Preconditions.checkNotNull(metadata);
+
+        DecoratedKey partitionKey = metadata.partitioner.decorateKey(decompose(metadata.partitionKeyType, key));
+        AccordKey.PartitionKey accordKey = new AccordKey.PartitionKey(metadata.id, partitionKey);
+
+        keys.add(accordKey);
+        updates.add(new AccordUpdate.AppendingUpdate(accordKey, appends));
+
+        return this;
+    }
+
+    public AccordTxnBuilder withAppend(String keyspace, String table, Object key, String column, String append)
+    {
+        Map<String, String> appends = new HashMap<>();
+        appends.put(column, append);
+        return withAppend(keyspace, table, key, appends);
+    }
+
+    public AccordTxnBuilder withIncrement(String keyspace, String table, Object key, Map<String, Integer> increments)
+    {
+        TableMetadata metadata = Schema.instance.getTableMetadata(keyspace, table);
+        Preconditions.checkNotNull(metadata);
+
+        DecoratedKey partitionKey = metadata.partitioner.decorateKey(decompose(metadata.partitionKeyType, key));
+        AccordKey.PartitionKey accordKey = new AccordKey.PartitionKey(metadata.id, partitionKey);
+
+        keys.add(accordKey);
+        updates.add(new AccordUpdate.IncrementingUpdate(accordKey, increments));
+
+        return this;
+    }
+
+    public AccordTxnBuilder withIncrement(String keyspace, String table, Object key, String column, int increment)
+    {
+        Map<String, Integer> increments = new HashMap<>();
+        increments.put(column, increment);
+        return withIncrement(keyspace, table, key, increments);
+    }
+
+
+    private static ByteBuffer decompose(AbstractType<?> type, Object value)
+    {
+        return ((AbstractType<Object>) type).decompose(value);
+    }
+
+    public AccordTxnBuilder withCondition(String keyspace, String table, Object key, Object clustering, String column, Type type, Object value)
+    {
+        Preconditions.checkState(!updates.isEmpty());
+        boolean isExistsPredicate = type == Type.EXISTS || type == Type.NOT_EXISTS;
+        Preconditions.checkArgument((value != null && !isExistsPredicate) || (value == null && isExistsPredicate));
+
+        TableMetadata metadata = Schema.instance.getTableMetadata(keyspace, table);
+        Preconditions.checkNotNull(metadata);
+
+        DecoratedKey decoratedKey = metadata.partitioner.decorateKey(decompose(metadata.partitionKeyType, key));
+        Clustering<ByteBuffer> clusteringBytes = Clustering.make(decompose(metadata.comparator.subtype(0), clustering));
+
+        if (isExistsPredicate)
+        {
+            predicates.add(new AccordUpdate.ExistsPredicate(type, metadata, decoratedKey, clusteringBytes));
+        }
+        else
+        {
+            ColumnMetadata columnMetadata = metadata.getColumn(new ColumnIdentifier(column, true));
+            Preconditions.checkNotNull(columnMetadata);
+            ByteBuffer valueBytes = decompose(columnMetadata.type, value);
+            predicates.add(new AccordUpdate.ValuePredicate(type, metadata, decoratedKey, clusteringBytes, columnMetadata, valueBytes));
+        }
+
+        return this;
+    }
+
+    public AccordTxnBuilder withCondition(String keyspace, String table, Object key, Object clustering, Type type)
+    {
+        return withCondition(keyspace, table, key, clustering, null, type, null);
+    }
+
+    public Txn build()
+    {
+        Key[] keyArray = keys.toArray(new Key[0]);
+        Arrays.sort(keyArray, AccordKey::compareKeys);
+        predicates.sort(Comparator.comparing(UpdatePredicate::partitionKey));
+        if (updates.isEmpty())
+        {
+            return new Txn.InMemory(Keys.of(keyArray), new AccordRead(reads), query);
+        }
+        else
+        {
+            List<UpdatePredicate> unsatisfiable = predicates.stream()
+                                                            .filter(predicate -> !reads.stream().anyMatch(predicate::supportedByRead))
+                                                            .collect(Collectors.toList());
+
+            if (!unsatisfiable.isEmpty())
+            {
+                String msg = String.format("Some predicates are unsupported by a read, and are unsatisfiable: %s", unsatisfiable);
+                throw new IllegalStateException(msg);
+            }
+
+            return new Txn.InMemory(Keys.of(keyArray), new AccordRead(reads), query, new AccordUpdate(updates, predicates));
+        }
+    }
+
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/shared/VersionedApplicationState.java b/src/java/org/apache/cassandra/service/accord/AccordVerbHandler.java
similarity index 50%
copy from test/distributed/org/apache/cassandra/distributed/shared/VersionedApplicationState.java
copy to src/java/org/apache/cassandra/service/accord/AccordVerbHandler.java
index fd3e40a70d..216d64df42 100644
--- a/test/distributed/org/apache/cassandra/distributed/shared/VersionedApplicationState.java
+++ b/src/java/org/apache/cassandra/service/accord/AccordVerbHandler.java
@@ -16,20 +16,33 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.distributed.shared;
+package org.apache.cassandra.service.accord;
 
-import java.io.Serializable;
+import java.io.IOException;
 
-public class VersionedApplicationState implements Serializable
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.local.Node;
+import accord.messages.Request;
+import org.apache.cassandra.net.IVerbHandler;
+import org.apache.cassandra.net.Message;
+
+public class AccordVerbHandler<T extends Request> implements IVerbHandler<T>
 {
-    public final int applicationState;
-    public final String value;
-    public final int version;
+    private static final Logger logger = LoggerFactory.getLogger(AccordVerbHandler.class);
+
+    private final Node node;
+
+    public AccordVerbHandler(Node node)
+    {
+        this.node = node;
+    }
 
-    public VersionedApplicationState(int applicationState, String value, int version)
+    @Override
+    public void doVerb(Message<T> message) throws IOException
     {
-        this.applicationState = applicationState;
-        this.value = value;
-        this.version = version;
+        logger.debug("Receiving {} from {}", message.payload, message.from());
+        message.payload.process(node, EndpointMapping.getId(message.from()), message);
     }
 }
diff --git a/src/java/org/apache/cassandra/service/accord/EndpointMapping.java b/src/java/org/apache/cassandra/service/accord/EndpointMapping.java
new file mode 100644
index 0000000000..d650db1abe
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/EndpointMapping.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+
+import accord.local.Node;
+import org.apache.cassandra.locator.InetAddressAndPort;
+import org.apache.cassandra.locator.Replica;
+
+public class EndpointMapping
+{
+    static Node.Id endpointToId(InetAddressAndPort endpoint)
+    {
+        Preconditions.checkArgument(endpoint.getAddress() instanceof Inet4Address);
+        Inet4Address address = (Inet4Address) endpoint.getAddress();
+        byte[] bytes = address.getAddress();
+        long id = 0;
+        for (int i=0; i<4; i++)
+            id = (id * 1000) + Byte.toUnsignedLong(bytes[i]);
+        id = (id * 100000) + endpoint.getPort();
+        return new Node.Id(id);
+    }
+
+    static InetAddressAndPort idToEndpoint(Node.Id node)
+    {
+        long id = node.id;
+        Preconditions.checkArgument(id >= 0);
+
+        int port = (int) (id % 100000);
+        id = id / 100000;
+        byte[] bytes = new byte[4];
+        for (int i=0; i<4; i++)
+        {
+            long octet = id % 1000;
+            Preconditions.checkArgument(octet >= 0 && octet <= 255, "Malformed id");
+            bytes[3-i] = (byte) (octet);
+            id = id / 1000;
+        }
+        Preconditions.checkArgument(id == 0);
+        try
+        {
+            return InetAddressAndPort.getByAddressOverrideDefaults(InetAddress.getByAddress(bytes), port);
+        }
+        catch (UnknownHostException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static class Mapping
+    {
+        private static final Mapping EMPTY = new Mapping(ImmutableMap.of(), ImmutableMap.of());
+        final ImmutableMap<Node.Id, InetAddressAndPort> idToEndpoint;
+        final ImmutableMap<InetAddressAndPort, Node.Id> endpointToId;
+
+        public Mapping(ImmutableMap<Node.Id, InetAddressAndPort> idToEndpoint,
+                       ImmutableMap<InetAddressAndPort, Node.Id> endpointToId)
+        {
+            this.idToEndpoint = idToEndpoint;
+            this.endpointToId = endpointToId;
+        }
+
+        private static <K, V> ImmutableMap<K, V> put(ImmutableMap<K, V> current, K key, V val)
+        {
+            return ImmutableMap.<K, V>builderWithExpectedSize(current.size() + 1).putAll(current).put(key, val).build();
+        }
+
+        public Mapping add(InetAddressAndPort endpoint)
+        {
+            if (endpointToId.containsKey(endpoint))
+                return this;
+            Node.Id id = endpointToId(endpoint);
+            return new Mapping(put(idToEndpoint, id, endpoint), put(endpointToId, endpoint, id));
+        }
+
+        public Mapping add(Node.Id id)
+        {
+            if (idToEndpoint.containsKey(id))
+                return this;
+
+            InetAddressAndPort endpoint = idToEndpoint(id);
+            return new Mapping(put(idToEndpoint, id, endpoint), put(endpointToId, endpoint, id));
+        }
+    }
+
+    private static volatile Mapping mapping = Mapping.EMPTY;
+
+    private EndpointMapping() {}
+
+    public static Node.Id getId(InetAddressAndPort endpoint)
+    {
+        Node.Id id = mapping.endpointToId.get(endpoint);
+        if (id == null)
+        {
+            synchronized (EndpointMapping.class)
+            {
+                mapping = mapping.add(endpoint);
+                id = mapping.endpointToId.get(endpoint);
+            }
+        }
+        return id;
+    }
+
+    // FIXME: put this stuff into the configuration service, where it will eventually live
+    public static Node.Id getId(Replica replica)
+    {
+        return getId(replica.endpoint());
+    }
+
+    public static InetAddressAndPort getEndpoint(Node.Id id)
+    {
+        InetAddressAndPort endpoint = mapping.idToEndpoint.get(id);
+        if (endpoint == null)
+        {
+            synchronized (EndpointMapping.class)
+            {
+                mapping = mapping.add(id);
+                endpoint = mapping.idToEndpoint.get(id);
+            }
+        }
+        return endpoint;
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/ListenerProxy.java b/src/java/org/apache/cassandra/service/accord/ListenerProxy.java
new file mode 100644
index 0000000000..2c9c5e07a9
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/ListenerProxy.java
@@ -0,0 +1,279 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.Objects;
+
+import com.google.common.collect.ImmutableList;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.local.Command;
+import accord.local.CommandStore;
+import accord.local.Listener;
+import accord.local.PreLoadContext;
+import accord.primitives.TxnId;
+import org.apache.cassandra.db.marshal.ByteBufferAccessor;
+import org.apache.cassandra.db.marshal.ValueAccessor;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.async.AsyncContext;
+import org.apache.cassandra.service.accord.serializers.CommandSerializers;
+import org.apache.cassandra.utils.ObjectSizes;
+
+public abstract class ListenerProxy implements Listener, Comparable<ListenerProxy>
+{
+    private static final Logger logger = LoggerFactory.getLogger(ListenerProxy.class);
+
+    public enum Kind { COMMAND, COMMANDS_FOR_KEY }
+
+    public abstract Kind kind();
+    public abstract ByteBuffer identifier();
+
+    final CommandStore commandStore;
+
+    private ListenerProxy(CommandStore commandStore)
+    {
+        this.commandStore = commandStore;
+    }
+
+    @Override
+    public int compareTo(ListenerProxy that)
+    {
+        return kind().compareTo(that.kind());
+    }
+
+    protected abstract long estimatedSizeOnHeap();
+
+    static class CommandListenerProxy extends ListenerProxy
+    {
+        private static final long EMPTY_SIZE = ObjectSizes.measure(new CommandListenerProxy(null, null));
+        private final TxnId txnId;
+
+        public CommandListenerProxy(CommandStore commandStore, TxnId txnId)
+        {
+            super(commandStore);
+            this.txnId = txnId;
+        }
+
+        @Override
+        public boolean equals(Object o)
+        {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            CommandListenerProxy that = (CommandListenerProxy) o;
+            return txnId.equals(that.txnId);
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return Objects.hash(txnId);
+        }
+
+        @Override
+        public int compareTo(ListenerProxy that)
+        {
+            int cmp = super.compareTo(that);
+            if (cmp != 0)
+                return cmp;
+
+            return this.txnId.compareTo(((CommandListenerProxy) that).txnId);
+        }
+
+        @Override
+        public String toString()
+        {
+            return "CommandListenerProxy{" +
+                   "txnId=" + txnId +
+                   '}';
+        }
+
+        @Override
+        public PreLoadContext listenerPreLoadContext(TxnId caller)
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Kind kind()
+        {
+            return Kind.COMMAND;
+        }
+
+        @Override
+        public ByteBuffer identifier()
+        {
+            ByteBuffer bytes = ByteBuffer.allocate(1 + CommandSerializers.txnId.serializedSize());
+            ByteBufferAccessor.instance.putByte(bytes, 0, (byte) kind().ordinal());
+            CommandSerializers.txnId.serialize(txnId, bytes, ByteBufferAccessor.instance, 1);
+            return bytes;
+        }
+
+        @Override
+        public void onChange(Command c)
+        {
+            AccordCommand command = (AccordCommand) c;
+            AccordCommandStore commandStore = command.commandStore();
+            AsyncContext context = commandStore.getContext();
+            PreLoadContext loadCtx = PreLoadContext.contextFor(ImmutableList.of(command.txnId(), txnId), Collections.emptyList());
+            if (context.containsScopedItems(loadCtx))
+            {
+                // TODO (soon): determine if this can break anything by not waiting for the current operation to denormalize it's data
+                //  the summary loader may default to commands in context, in case it wouldn't
+                logger.trace("{}: synchronously updating listening command {}", c.txnId(), txnId);
+                commandStore.command(txnId).onChange(c);
+            }
+            else
+            {
+                TxnId callingTxnId = command.txnId();
+                logger.trace("{}: asynchronously updating listening command {}", c.txnId(), txnId);
+                commandStore.process(loadCtx, instance -> {
+                    Command caller = instance.command(callingTxnId);
+                    commandStore.command(txnId).onChange(caller);
+                });
+            }
+        }
+
+        @Override
+        protected long estimatedSizeOnHeap()
+        {
+            return EMPTY_SIZE + AccordObjectSizes.timestamp(txnId);
+        }
+    }
+
+    /**
+     * These always need to be run in either the same task as the notifying command, or immediately afterwards, otherwise we
+     * may use stale max timestamps for preaccept
+     */
+    static class CommandsForKeyListenerProxy extends ListenerProxy
+    {
+        private static final long EMPTY_SIZE = ObjectSizes.measure(new CommandsForKeyListenerProxy(null, null));
+        private final AccordKey.PartitionKey key;
+
+        public CommandsForKeyListenerProxy(CommandStore commandStore, AccordKey.PartitionKey key)
+        {
+            super(commandStore);
+            this.key = key;
+        }
+
+        @Override
+        public boolean equals(Object o)
+        {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            CommandsForKeyListenerProxy that = (CommandsForKeyListenerProxy) o;
+            return key.equals(that.key);
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return Objects.hash(key);
+        }
+
+        @Override
+        public int compareTo(ListenerProxy that)
+        {
+            int cmp = super.compareTo(that);
+            if (cmp != 0)
+                return cmp;
+
+            return this.key.compareTo(((CommandsForKeyListenerProxy) that).key);
+        }
+
+        @Override
+        public String toString()
+        {
+            return "CommandsForKeyListenerProxy{" +
+                   "key=" + key +
+                   '}';
+        }
+
+        @Override
+        public PreLoadContext listenerPreLoadContext(TxnId caller)
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Kind kind()
+        {
+            return Kind.COMMANDS_FOR_KEY;
+        }
+
+        @Override
+        public ByteBuffer identifier()
+        {
+            ByteBuffer bytes = ByteBuffer.allocate((int) (1 + AccordKey.PartitionKey.serializer.serializedSize(key)));
+            ByteBufferAccessor.instance.putByte(bytes, 0, (byte) kind().ordinal());
+            AccordKey.PartitionKey.serializer.serialize(key, bytes, ByteBufferAccessor.instance, 1);
+            return bytes;
+        }
+
+        @Override
+        public void onChange(Command c)
+        {
+            AccordCommand command = (AccordCommand) c;
+            AccordCommandStore commandStore = command.commandStore();
+            AsyncContext context = commandStore.getContext();
+            PreLoadContext loadCtx = PreLoadContext.contextFor(ImmutableList.of(command.txnId()), ImmutableList.of(key));
+            if (context.containsScopedItems(loadCtx))
+            {
+                logger.trace("{}: synchronously updating listening cfk {}", c.txnId(), key);
+                commandStore.commandsForKey(key).onChange(c);
+            }
+            else
+            {
+                TxnId callingTxnId = command.txnId();
+                logger.trace("{}: asynchronously updating listening cfk {}", c.txnId(), key);
+                commandStore.process(loadCtx, instance -> {
+                    Command caller = instance.command(callingTxnId);
+                    commandStore.commandsForKey(key).onChange(caller);
+                });
+            }
+        }
+
+        @Override
+        protected long estimatedSizeOnHeap()
+        {
+            return EMPTY_SIZE + key.estimatedSizeOnHeap();
+        }
+    }
+
+    public static <V> ListenerProxy deserialize(CommandStore commandStore, V src, ValueAccessor<V> accessor, int offset) throws IOException
+    {
+        int ordinal = accessor.getByte(src, offset);
+        Kind kind = Kind.values()[ordinal];
+        offset += 1;
+        switch (kind)
+        {
+            case COMMAND:
+                TxnId txnId = CommandSerializers.txnId.deserialize(src, accessor, offset);
+                return new CommandListenerProxy(commandStore, txnId);
+            case COMMANDS_FOR_KEY:
+                AccordKey.PartitionKey key = AccordKey.PartitionKey.serializer.deserialize(src, accessor, offset);
+                return new CommandsForKeyListenerProxy(commandStore, key);
+            default:
+                throw new IOException("Unknown kind ordinal " + ordinal);
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/ReadFuture.java b/src/java/org/apache/cassandra/service/accord/ReadFuture.java
new file mode 100644
index 0000000000..46eb4f86ad
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/ReadFuture.java
@@ -0,0 +1,295 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import com.google.common.util.concurrent.FutureCallback;
+
+import accord.api.Data;
+import accord.primitives.Keys;
+import io.netty.util.concurrent.GenericFutureListener;
+import org.apache.cassandra.utils.concurrent.Future;
+import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
+
+public class ReadFuture implements Future<Data>
+{
+    final Keys scope;
+    private final Future<Data> wrappped;
+
+    public ReadFuture(Keys scope, Future<Data> wrappped)
+    {
+        this.scope = scope;
+        this.wrappped = wrappped;
+    }
+
+    @Override
+    public Future<Data> await() throws InterruptedException
+    {
+        return wrappped.await();
+    }
+
+    @Override
+    public Future<Data> awaitUninterruptibly()
+    {
+        return wrappped.awaitUninterruptibly();
+    }
+
+    @Override
+    public Future<Data> awaitThrowUncheckedOnInterrupt()
+    {
+        return wrappped.awaitThrowUncheckedOnInterrupt();
+    }
+
+    @Override
+    public void rethrowIfFailed()
+    {
+        wrappped.rethrowIfFailed();
+    }
+
+    @Override
+    public Future<Data> sync() throws InterruptedException
+    {
+        return wrappped.sync();
+    }
+
+    @Override
+    public Future<Data> syncUninterruptibly()
+    {
+        return wrappped.syncUninterruptibly();
+    }
+
+    @Override
+    public Future<Data> syncThrowUncheckedOnInterrupt()
+    {
+        return wrappped.syncThrowUncheckedOnInterrupt();
+    }
+
+    @Override
+    @Deprecated
+    public boolean await(long l) throws InterruptedException
+    {
+        return wrappped.await(l);
+    }
+
+    @Override
+    @Deprecated
+    public boolean awaitUninterruptibly(long l)
+    {
+        return wrappped.awaitUninterruptibly(l);
+    }
+
+    @Override
+    public Future<Data> addCallback(BiConsumer<? super Data, Throwable> callback)
+    {
+        return wrappped.addCallback(callback);
+    }
+
+    @Override
+    public Future<Data> addCallback(BiConsumer<? super Data, Throwable> callback, Executor executor)
+    {
+        return wrappped.addCallback(callback, executor);
+    }
+
+    @Override
+    public Future<Data> addCallback(FutureCallback<? super Data> callback)
+    {
+        return wrappped.addCallback(callback);
+    }
+
+    @Override
+    public Future<Data> addCallback(FutureCallback<? super Data> callback, Executor executor)
+    {
+        return wrappped.addCallback(callback, executor);
+    }
+
+    @Override
+    public Future<Data> addCallback(Consumer<? super Data> onSuccess, Consumer<? super Throwable> onFailure)
+    {
+        return wrappped.addCallback(onSuccess, onFailure);
+    }
+
+    @Override
+    public Future<Data> addCallback(Consumer<? super Data> onSuccess, Consumer<? super Throwable> onFailure, Executor executor)
+    {
+        return wrappped.addCallback(onSuccess, onFailure, executor);
+    }
+
+    @Override
+    public <T> Future<T> map(Function<? super Data, ? extends T> mapper)
+    {
+        return wrappped.map(mapper);
+    }
+
+    @Override
+    public <T> Future<T> map(Function<? super Data, ? extends T> mapper, Executor executor)
+    {
+        return wrappped.map(mapper, executor);
+    }
+
+    @Override
+    public <T> Future<T> flatMap(Function<? super Data, ? extends Future<T>> flatMapper)
+    {
+        return wrappped.flatMap(flatMapper);
+    }
+
+    @Override
+    public <T> Future<T> flatMap(Function<? super Data, ? extends Future<T>> flatMapper, Executor executor)
+    {
+        return wrappped.flatMap(flatMapper, executor);
+    }
+
+    @Override
+    public void addListener(Runnable runnable, Executor executor)
+    {
+        wrappped.addListener(runnable, executor);
+    }
+
+    @Override
+    public void addListener(Runnable runnable)
+    {
+        wrappped.addListener(runnable);
+    }
+
+    @Override
+    public Executor notifyExecutor()
+    {
+        return wrappped.notifyExecutor();
+    }
+
+    @Override
+    public Future<Data> addListener(GenericFutureListener<? extends io.netty.util.concurrent.Future<? super Data>> genericFutureListener)
+    {
+        return wrappped.addListener(genericFutureListener);
+    }
+
+    @Override
+    public Future<Data> addListeners(GenericFutureListener<? extends io.netty.util.concurrent.Future<? super Data>>... genericFutureListeners)
+    {
+        return wrappped.addListeners(genericFutureListeners);
+    }
+
+    @Override
+    public Future<Data> removeListener(GenericFutureListener<? extends io.netty.util.concurrent.Future<? super Data>> genericFutureListener)
+    {
+        return wrappped.removeListener(genericFutureListener);
+    }
+
+    @Override
+    public Future<Data> removeListeners(GenericFutureListener<? extends io.netty.util.concurrent.Future<? super Data>>... genericFutureListeners)
+    {
+        return wrappped.removeListeners(genericFutureListeners);
+    }
+
+    @Override
+    public boolean isSuccess()
+    {
+        return wrappped.isSuccess();
+    }
+
+    @Override
+    public boolean isCancellable()
+    {
+        return wrappped.isCancellable();
+    }
+
+    @Override
+    public Throwable cause()
+    {
+        return wrappped.cause();
+    }
+
+    @Override
+    public boolean await(long timeout, TimeUnit unit) throws InterruptedException
+    {
+        return wrappped.await(timeout, unit);
+    }
+
+    @Override
+    public boolean awaitUninterruptibly(long timeout, TimeUnit unit)
+    {
+        return wrappped.awaitUninterruptibly(timeout, unit);
+    }
+
+    @Override
+    public Data getNow()
+    {
+        return wrappped.getNow();
+    }
+
+    @Override
+    public boolean cancel(boolean mayInterruptIfRunning)
+    {
+        return wrappped.cancel(mayInterruptIfRunning);
+    }
+
+    @Override
+    public boolean isCancelled()
+    {
+        return wrappped.isCancelled();
+    }
+
+    @Override
+    public boolean isDone()
+    {
+        return wrappped.isDone();
+    }
+
+    @Override
+    public Data get() throws InterruptedException, ExecutionException
+    {
+        return wrappped.get();
+    }
+
+    @Override
+    public Data get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
+    {
+        return wrappped.get(timeout, unit);
+    }
+
+    @Override
+    public boolean awaitUntil(long nanoTimeDeadline) throws InterruptedException
+    {
+        return wrappped.awaitUntil(nanoTimeDeadline);
+    }
+
+    @Override
+    public boolean awaitUntilThrowUncheckedOnInterrupt(long nanoTimeDeadline) throws UncheckedInterruptedException
+    {
+        return wrappped.awaitUntilThrowUncheckedOnInterrupt(nanoTimeDeadline);
+    }
+
+    @Override
+    public boolean awaitUntilUninterruptibly(long nanoTimeDeadline)
+    {
+        return wrappped.awaitUntilUninterruptibly(nanoTimeDeadline);
+    }
+
+    @Override
+    public boolean awaitThrowUncheckedOnInterrupt(long time, TimeUnit units) throws UncheckedInterruptedException
+    {
+        return wrappped.awaitThrowUncheckedOnInterrupt(time, units);
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/TokenRange.java b/src/java/org/apache/cassandra/service/accord/TokenRange.java
new file mode 100644
index 0000000000..0f8d7d5293
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/TokenRange.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord;
+
+import java.io.IOException;
+
+import com.google.common.base.Preconditions;
+
+import accord.api.Key;
+import accord.primitives.KeyRange;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.schema.TableId;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.api.AccordKey.SentinelKey;
+import org.apache.cassandra.service.accord.api.AccordKey.TokenKey;
+
+public class TokenRange extends KeyRange.EndInclusive
+{
+    public TokenRange(AccordKey start, AccordKey end)
+    {
+        super(start, end);
+        Preconditions.checkArgument(start.kind().supportsRanges());
+        Preconditions.checkArgument(end.kind().supportsRanges());
+    }
+
+    private static AccordKey toAccordTokenOrSentinel(Key key)
+    {
+        AccordKey aKey = (AccordKey) key;
+        if (aKey instanceof TokenKey || aKey instanceof SentinelKey)
+            return aKey;
+        return new TokenKey(aKey.tableId(), aKey.partitionKey().getToken().maxKeyBound());
+    }
+
+    public static TokenRange fullRange(TableId tableId)
+    {
+        return new TokenRange(SentinelKey.min(tableId), SentinelKey.max(tableId));
+    }
+
+    @Override
+    public TokenRange subRange(Key start, Key end)
+    {
+        return new TokenRange(toAccordTokenOrSentinel(start), toAccordTokenOrSentinel(end));
+    }
+
+    public static final IVersionedSerializer<TokenRange> serializer = new IVersionedSerializer<TokenRange>()
+    {
+        @Override
+        public void serialize(TokenRange range, DataOutputPlus out, int version) throws IOException
+        {
+            AccordKey.serializer.serialize((AccordKey) range.start(), out, version);
+            AccordKey.serializer.serialize((AccordKey) range.end(), out, version);
+        }
+
+        @Override
+        public TokenRange deserialize(DataInputPlus in, int version) throws IOException
+        {
+            return new TokenRange(AccordKey.serializer.deserialize(in, version),
+                                  AccordKey.serializer.deserialize(in, version));
+        }
+
+        @Override
+        public long serializedSize(TokenRange range, int version)
+        {
+            return AccordKey.serializer.serializedSize((AccordKey) range.start(), version)
+                 + AccordKey.serializer.serializedSize((AccordKey) range.end(), version);
+        }
+    };
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/shared/VersionedApplicationState.java b/src/java/org/apache/cassandra/service/accord/api/AccordAgent.java
similarity index 59%
copy from test/distributed/org/apache/cassandra/distributed/shared/VersionedApplicationState.java
copy to src/java/org/apache/cassandra/service/accord/api/AccordAgent.java
index fd3e40a70d..a5e94d550f 100644
--- a/test/distributed/org/apache/cassandra/distributed/shared/VersionedApplicationState.java
+++ b/src/java/org/apache/cassandra/service/accord/api/AccordAgent.java
@@ -16,20 +16,31 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.distributed.shared;
+package org.apache.cassandra.service.accord.api;
 
-import java.io.Serializable;
+import accord.api.Agent;
+import accord.api.Result;
+import accord.local.Command;
+import accord.local.Node;
+import accord.primitives.Timestamp;
 
-public class VersionedApplicationState implements Serializable
+public class AccordAgent implements Agent
 {
-    public final int applicationState;
-    public final String value;
-    public final int version;
+    @Override
+    public void onRecover(Node node, Result success, Throwable fail)
+    {
+        // TODO: this
+    }
+
+    @Override
+    public void onInconsistentTimestamp(Command command, Timestamp prev, Timestamp next)
+    {
+        // TODO: this
+    }
 
-    public VersionedApplicationState(int applicationState, String value, int version)
+    @Override
+    public void onUncaughtException(Throwable t)
     {
-        this.applicationState = applicationState;
-        this.value = value;
-        this.version = version;
+        // TODO: this
     }
 }
diff --git a/src/java/org/apache/cassandra/service/accord/api/AccordKey.java b/src/java/org/apache/cassandra/service/accord/api/AccordKey.java
new file mode 100644
index 0000000000..845d43cec0
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/api/AccordKey.java
@@ -0,0 +1,480 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord.api;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+import com.google.common.base.Preconditions;
+
+import accord.api.Key;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.db.SinglePartitionReadCommand;
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.db.marshal.ByteBufferAccessor;
+import org.apache.cassandra.db.marshal.ValueAccessor;
+import org.apache.cassandra.db.partitions.Partition;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.schema.Schema;
+import org.apache.cassandra.schema.TableId;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.ObjectSizes;
+
+public interface AccordKey extends Key
+{
+    enum Kind
+    {
+        TOKEN, PARTITION, SENTINEL;
+
+        public boolean supportsRanges()
+        {
+            switch (this)
+            {
+                case SENTINEL:
+                case TOKEN:
+                    return true;
+                default:
+                    return false;
+            }
+        }
+    };
+
+    TableId tableId();
+    PartitionPosition partitionKey();
+    Kind kind();
+
+    static AccordKey of(Key key)
+    {
+        return (AccordKey) key;
+    }
+
+    static PartitionKey of(Partition partition)
+    {
+        return new PartitionKey(partition.metadata().id, partition.partitionKey());
+    }
+
+    static PartitionKey of(SinglePartitionReadCommand command)
+    {
+        return new PartitionKey(command.metadata().id, command.partitionKey());
+    }
+
+    static int compare(AccordKey left, AccordKey right)
+    {
+        int cmp = left.tableId().compareTo(right.tableId());
+        if (cmp != 0)
+            return cmp;
+
+        if (left instanceof SentinelKey || right instanceof SentinelKey)
+        {
+            int leftInt = left instanceof SentinelKey ? ((SentinelKey) left).asInt() : 0;
+            int rightInt = right instanceof SentinelKey ? ((SentinelKey) right).asInt() : 0;
+            return Integer.compare(leftInt, rightInt);
+        }
+
+        return left.partitionKey().compareTo(right.partitionKey());
+    }
+
+    static int compareKeys(Key left, Key right)
+    {
+        return compare((AccordKey) left, (AccordKey) right);
+    }
+
+    default int compareTo(AccordKey that)
+    {
+        return compare(this, that);
+    }
+
+    @Override
+    default int routingHash()
+    {
+        return partitionKey().getToken().tokenHash();
+    }
+
+    class SentinelKey implements AccordKey
+    {
+        private final TableId tableId;
+        private final boolean isMin;
+
+        private SentinelKey(TableId tableId, boolean isMin)
+        {
+            this.tableId = tableId;
+            this.isMin = isMin;
+        }
+
+        @Override
+        public boolean equals(Object o)
+        {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            SentinelKey that = (SentinelKey) o;
+            return isMin == that.isMin && tableId.equals(that.tableId);
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return Objects.hash(tableId, isMin);
+        }
+
+        @Override
+        public int compareTo(Key that)
+        {
+            return compare(this, (AccordKey) that);
+        }
+
+        @Override
+        public Kind kind()
+        {
+            return Kind.SENTINEL;
+        }
+
+        public static SentinelKey min(TableId tableId)
+        {
+            return new SentinelKey(tableId, true);
+        }
+
+        public static SentinelKey max(TableId tableId)
+        {
+            return new SentinelKey(tableId, false);
+        }
+
+        @Override
+        public TableId tableId()
+        {
+            return tableId;
+        }
+
+        @Override
+        public PartitionPosition partitionKey()
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        int asInt()
+        {
+            return isMin ? -1 : 1;
+        }
+
+        @Override
+        public String toString()
+        {
+            return "SentinelKey{" +
+                   "tableId=" + tableId +
+                   ", key=" + (isMin ? "min": "max") +
+                   '}';
+        }
+
+        public static final IVersionedSerializer<SentinelKey> serializer = new IVersionedSerializer<SentinelKey>()
+        {
+            @Override
+            public void serialize(SentinelKey key, DataOutputPlus out, int version) throws IOException
+            {
+                out.writeBoolean(key.isMin);
+                key.tableId().serialize(out);
+            }
+
+            @Override
+            public SentinelKey deserialize(DataInputPlus in, int version) throws IOException
+            {
+                boolean isMin = in.readBoolean();
+                TableId tableId = TableId.deserialize(in);
+                return new SentinelKey(tableId, isMin);
+            }
+
+            @Override
+            public long serializedSize(SentinelKey key, int version)
+            {
+                return TypeSizes.BOOL_SIZE + TableId.serializedSize();
+            }
+        };
+    }
+
+    abstract class AbstractKey<T extends PartitionPosition> implements AccordKey
+    {
+        private final TableId tableId;
+        private final T key;
+
+        public AbstractKey(TableId tableId, T key)
+        {
+            this.tableId = tableId;
+            this.key = key;
+        }
+
+        @Override
+        public boolean equals(Object o)
+        {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            AbstractKey<?> that = (AbstractKey<?>) o;
+            return tableId.equals(that.tableId) && key.equals(that.key);
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return Objects.hash(tableId, key);
+        }
+
+        @Override
+        public int compareTo(Key that)
+        {
+            return compare(this, (AccordKey) that);
+        }
+
+        @Override
+        public TableId tableId()
+        {
+            return tableId;
+        }
+
+        @Override
+        public T partitionKey()
+        {
+            return key;
+        }
+    }
+
+    class PartitionKey extends AbstractKey<DecoratedKey>
+    {
+        private static final long EMPTY_SIZE;
+        static
+        {
+            DecoratedKey key = DatabaseDescriptor.getPartitioner().decorateKey(ByteBufferUtil.EMPTY_BYTE_BUFFER);
+            EMPTY_SIZE = ObjectSizes.measureDeep(new PartitionKey(null, key));
+        }
+
+        public PartitionKey(TableId tableId, DecoratedKey key)
+        {
+            super(tableId, key);
+        }
+
+        @Override
+        public Kind kind()
+        {
+            return Kind.PARTITION;
+        }
+
+        @Override
+        public String toString()
+        {
+            return "PartitionKey{" +
+                   "tableId=" + tableId() +
+                   ", key=" + partitionKey() +
+                   '}';
+        }
+
+        public long estimatedSizeOnHeap()
+        {
+            return EMPTY_SIZE + ByteBufferAccessor.instance.size(partitionKey().getKey());
+        }
+
+        public static final Serializer serializer = new Serializer();
+        public static class Serializer implements IVersionedSerializer<PartitionKey>
+        {
+            // TODO: add vint to value accessor and use vints
+            private Serializer() {}
+
+            @Override
+            public void serialize(PartitionKey key, DataOutputPlus out, int version) throws IOException
+            {
+                key.tableId().serialize(out);
+                ByteBufferUtil.writeWithShortLength(key.partitionKey().getKey(), out);
+            }
+
+            public <V> int serialize(PartitionKey key, V dst, ValueAccessor<V> accessor, int offset)
+            {
+                int position = offset;
+                position += key.tableId().serialize(dst, accessor, position);
+                ByteBuffer bytes = key.partitionKey().getKey();
+                int numBytes = ByteBufferAccessor.instance.size(bytes);
+                Preconditions.checkState(numBytes <= Short.MAX_VALUE);
+                position += accessor.putShort(dst, position, (short) numBytes);
+                position += accessor.copyByteBufferTo(bytes, 0, dst, position, numBytes);
+                return position - offset;
+
+            }
+
+            @Override
+            public PartitionKey deserialize(DataInputPlus in, int version) throws IOException
+            {
+                TableId tableId = TableId.deserialize(in);
+                TableMetadata metadata = Schema.instance.getTableMetadata(tableId);
+                DecoratedKey key = metadata.partitioner.decorateKey(ByteBufferUtil.readWithShortLength(in));
+                return new PartitionKey(tableId, key);
+            }
+
+            public <V> PartitionKey deserialize(V src, ValueAccessor<V> accessor, int offset) throws IOException
+            {
+                TableId tableId = TableId.deserialize(src, accessor, offset);
+                offset += TableId.serializedSize();
+                TableMetadata metadata = Schema.instance.getTableMetadata(tableId);
+                int numBytes = accessor.getShort(src, offset);
+                offset += TypeSizes.SHORT_SIZE;
+                ByteBuffer bytes = ByteBuffer.allocate(numBytes);
+                accessor.copyTo(src, offset, bytes, ByteBufferAccessor.instance, 0, numBytes);
+                DecoratedKey key = metadata.partitioner.decorateKey(bytes);
+                return new PartitionKey(tableId, key);
+            }
+
+            @Override
+            public long serializedSize(PartitionKey key, int version)
+            {
+                return serializedSize(key);
+            }
+
+            public long serializedSize(PartitionKey key)
+            {
+                return key.tableId().serializedSize() + ByteBufferUtil.serializedSizeWithShortLength(key.partitionKey().getKey());
+            }
+        }
+    }
+
+    class TokenKey extends AbstractKey<Token.KeyBound>
+    {
+        public TokenKey(TableId tableId, Token.KeyBound key)
+        {
+            super(tableId, key);
+        }
+
+        public static TokenKey min(TableId tableId, Token token)
+        {
+            return new TokenKey(tableId, token.minKeyBound());
+        }
+
+        public static TokenKey max(TableId tableId, Token token)
+        {
+            return new TokenKey(tableId, token.maxKeyBound());
+        }
+
+        @Override
+        public Kind kind()
+        {
+            return Kind.TOKEN;
+        }
+
+        @Override
+        public String toString()
+        {
+            return "TokenKey{" +
+                   "tableId=" + tableId() +
+                   ", key=" + partitionKey() +
+                   '}';
+        }
+
+        public static final IVersionedSerializer<TokenKey> serializer = new IVersionedSerializer<TokenKey>()
+        {
+            @Override
+            public void serialize(TokenKey key, DataOutputPlus out, int version) throws IOException
+            {
+                key.tableId().serialize(out);
+                Token.KeyBound bound = key.partitionKey();
+                out.writeBoolean(bound.isMinimumBound);
+                Token.serializer.serialize(bound.getToken(), out, version);
+            }
+
+            @Override
+            public TokenKey deserialize(DataInputPlus in, int version) throws IOException
+            {
+                TableId tableId = TableId.deserialize(in);
+                TableMetadata metadata = Schema.instance.getTableMetadata(tableId);
+                boolean isMinimumBound = in.readBoolean();
+                Token token = Token.serializer.deserialize(in, metadata.partitioner, version);
+                return new TokenKey(tableId, isMinimumBound ? token.minKeyBound() : token.maxKeyBound());
+            }
+
+            @Override
+            public long serializedSize(TokenKey key, int version)
+            {
+                Token.KeyBound bound = key.partitionKey();
+                return key.tableId().serializedSize()
+                     + TypeSizes.sizeof(bound.isMinimumBound)
+                     + Token.serializer.serializedSize(bound.getToken(), version);
+            }
+        };
+    }
+
+    IVersionedSerializer<AccordKey> serializer = new IVersionedSerializer<AccordKey>()
+    {
+        @Override
+        public void serialize(AccordKey key, DataOutputPlus out, int version) throws IOException
+        {
+            out.write(key.kind().ordinal());
+            switch (key.kind())
+            {
+                case TOKEN:
+                    TokenKey.serializer.serialize((TokenKey) key, out, version);
+                    break;
+                case PARTITION:
+                    PartitionKey.serializer.serialize((PartitionKey) key, out, version);
+                    break;
+                case SENTINEL:
+                    SentinelKey.serializer.serialize((SentinelKey) key, out, version);
+                    break;
+                default:
+                    throw new IllegalArgumentException();
+            }
+
+        }
+
+        @Override
+        public AccordKey deserialize(DataInputPlus in, int version) throws IOException
+        {
+            Kind kind = Kind.values()[in.readByte()];
+            switch (kind)
+            {
+                case TOKEN:
+                    return TokenKey.serializer.deserialize(in, version);
+                case PARTITION:
+                    return PartitionKey.serializer.deserialize(in, version);
+                case SENTINEL:
+                    return SentinelKey.serializer.deserialize(in, version);
+                default:
+                    throw new IllegalArgumentException();
+            }
+        }
+
+        @Override
+        public long serializedSize(AccordKey key, int version)
+        {
+            long size = TypeSizes.BYTE_SIZE; // kind ordinal
+            switch (key.kind())
+            {
+                case TOKEN:
+                    size += TokenKey.serializer.serializedSize((TokenKey) key, version);
+                    break;
+                case PARTITION:
+                    size += PartitionKey.serializer.serializedSize((PartitionKey) key, version);
+                    break;
+                case SENTINEL:
+                    size += SentinelKey.serializer.serializedSize((SentinelKey) key, version);
+                    break;
+                default:
+                    throw new IllegalArgumentException();
+            }
+            return size;
+        }
+    };
+}
diff --git a/src/java/org/apache/cassandra/service/accord/api/AccordScheduler.java b/src/java/org/apache/cassandra/service/accord/api/AccordScheduler.java
new file mode 100644
index 0000000000..df83a92932
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/api/AccordScheduler.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord.api;
+
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import accord.api.Scheduler;
+import org.apache.cassandra.concurrent.ExecutorFactory;
+import org.apache.cassandra.concurrent.ScheduledExecutorPlus;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+public class AccordScheduler implements Scheduler, Shutdownable
+{
+    private final ScheduledExecutorPlus scheduledExecutor = ExecutorFactory.Global.executorFactory().scheduled("AccordScheduled");
+
+    private static class ScheduledFutureWrapper implements Scheduled
+    {
+        private final ScheduledFuture<?> future;
+
+        public ScheduledFutureWrapper(ScheduledFuture<?> future)
+        {
+            this.future = future;
+        }
+
+        @Override
+        public void cancel()
+        {
+            future.cancel(false);
+        }
+    }
+
+    @Override
+    public Scheduled recurring(Runnable run, long delay, TimeUnit units)
+    {
+        ScheduledFuture<?> future = scheduledExecutor.scheduleAtFixedRate(run, delay, delay, units);
+        return new ScheduledFutureWrapper(future);
+    }
+
+    @Override
+    public Scheduled once(Runnable run, long delay, TimeUnit units)
+    {
+        ScheduledFuture<?> future = scheduledExecutor.schedule(run, delay, units);
+        return new ScheduledFutureWrapper(future);
+    }
+
+    @Override
+    public void now(Runnable task)
+    {
+        // called from the mutation stage configured by the verb
+        if (scheduledExecutor.isShutdown())
+            throw new RejectedExecutionException("Scheduler has shut down.");
+        scheduledExecutor.submit(task);
+    }
+
+    @Override
+    public boolean isTerminated()
+    {
+        return scheduledExecutor.isTerminated();
+    }
+
+    @Override
+    public void shutdown()
+    {
+        scheduledExecutor.shutdown();
+    }
+
+    @Override
+    public List<Runnable> shutdownNow()
+    {
+        return scheduledExecutor.shutdownNow();
+    }
+
+    @Override
+    public boolean awaitTermination(long timeout, TimeUnit units) throws InterruptedException
+    {
+        return scheduledExecutor.awaitTermination(timeout, units);
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/async/AsyncContext.java b/src/java/org/apache/cassandra/service/accord/async/AsyncContext.java
new file mode 100644
index 0000000000..0b3c87536f
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/async/AsyncContext.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord.async;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
+
+import accord.local.PreLoadContext;
+import accord.primitives.TxnId;
+import org.apache.cassandra.service.accord.AccordCommand;
+import org.apache.cassandra.service.accord.AccordCommandStore;
+import org.apache.cassandra.service.accord.AccordCommandsForKey;
+import org.apache.cassandra.service.accord.AccordPartialCommand;
+import org.apache.cassandra.service.accord.AccordState;
+import org.apache.cassandra.service.accord.AccordStateCache;
+import org.apache.cassandra.service.accord.AccordState.WriteOnly;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+
+public class AsyncContext
+{
+    public static class Group<K, V extends AccordState<K>>
+    {
+        final Map<K, V> items = new HashMap<>();
+        final Map<K, WriteOnly<K, V>> writeOnly = new HashMap<>();
+
+        @VisibleForTesting
+        public void add(V item)
+        {
+            items.put(item.key(), item);
+        }
+
+        public V get(K key)
+        {
+            return items.get(key);
+        }
+
+        void releaseResources(AccordStateCache.Instance<K, V> cache)
+        {
+            items.values().forEach(cache::release);
+            items.clear();
+            writeOnly.clear();
+        }
+
+        public WriteOnly<K, V> getOrCreateWriteOnly(K key, BiFunction<AccordCommandStore, K, WriteOnly<K, V>> factory, AccordCommandStore commandStore)
+        {
+            Preconditions.checkState(!items.containsKey(key));
+            WriteOnly<K, V> command = writeOnly.get(key);
+            if (command == null)
+            {
+                command = factory.apply(commandStore, key);
+                writeOnly.put(key, command);
+            }
+            return command;
+        }
+    }
+
+    public static class CommandGroup extends Group<TxnId, AccordCommand>
+    {
+        List<AccordPartialCommand> partials = new ArrayList<>();
+
+        public void addPartialCommand(AccordPartialCommand partial)
+        {
+            partials.add(partial);
+        }
+
+        @Override
+        void releaseResources(AccordStateCache.Instance<TxnId, AccordCommand> cache)
+        {
+            super.releaseResources(cache);
+            partials.clear();
+        }
+    }
+
+    public final CommandGroup commands = new CommandGroup();
+    public final Group<PartitionKey, AccordCommandsForKey> commandsForKey = new Group<>();
+
+    public boolean containsScopedItems(PreLoadContext loadContext)
+    {
+        return Iterables.all(loadContext.txnIds(), commands.items::containsKey) && Iterables.all(loadContext.keys(), commandsForKey.items::containsKey);
+    }
+
+    void verifyLoaded()
+    {
+        commands.items.forEach((key, command) -> Preconditions.checkState(command.isLoaded()));
+        commandsForKey.items.forEach((key, cfk) -> Preconditions.checkState(cfk.isLoaded()));
+    }
+
+    void releaseResources(AccordCommandStore commandStore)
+    {
+        commands.releaseResources(commandStore.commandCache());
+        commandsForKey.releaseResources(commandStore.commandsForKeyCache());
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/async/AsyncLoader.java b/src/java/org/apache/cassandra/service/accord/async/AsyncLoader.java
new file mode 100644
index 0000000000..2cdb099394
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/async/AsyncLoader.java
@@ -0,0 +1,240 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord.async;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.primitives.TxnId;
+import org.apache.cassandra.concurrent.Stage;
+import org.apache.cassandra.service.accord.AccordCommand;
+import org.apache.cassandra.service.accord.AccordCommandStore;
+import org.apache.cassandra.service.accord.AccordCommandsForKey;
+import org.apache.cassandra.service.accord.AccordKeyspace;
+import org.apache.cassandra.service.accord.AccordStateCache;
+import org.apache.cassandra.service.accord.AccordState;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.utils.concurrent.Future;
+import org.apache.cassandra.utils.concurrent.FutureCombiner;
+
+
+public class AsyncLoader
+{
+    private static final Logger logger = LoggerFactory.getLogger(AsyncLoader.class);
+    enum State
+    {
+        INITIALIZED,
+        SETUP,
+        LOADING,
+        FINISHED
+    }
+
+    private State state = State.INITIALIZED;
+    private final AccordCommandStore commandStore;
+
+    private final Iterable<TxnId> txnIds;
+    private final Iterable<PartitionKey> keys;
+
+    protected Future<?> readFuture;
+
+    public AsyncLoader(AccordCommandStore commandStore, Iterable<TxnId> txnIds, Iterable<PartitionKey> keys)
+    {
+        this.commandStore = commandStore;
+        this.txnIds = txnIds;
+        this.keys = keys;
+    }
+
+    private <K, V extends AccordState<K>> Future<?> referenceAndDispatch(K key,
+                                                                         AccordStateCache.Instance<K, V> cache,
+                                                                         Map<K, V> context,
+                                                                         Function<V, Future<?>> readFunction,
+                                                                         Object callback)
+    {
+        V item;
+        Future<?> future = cache.getLoadFuture(key);
+        if (future != null)
+        {
+            // if a load future exists for this, it must be present in the cache
+            item = cache.getOrNull(key);
+            Preconditions.checkState(item != null);
+            context.put(key, item);
+            if (logger.isTraceEnabled())
+                logger.trace("Existing load future found for {} while loading for {}. ({})", item.key(), callback, item);
+            return future;
+        }
+
+        item = cache.getOrCreate(key);
+        context.put(key, item);
+        if (item.isLoaded())
+        {
+            if (logger.isTraceEnabled())
+                logger.trace("Cached item found for {} while loading for {}. ({})", item.key(), callback, item);
+            return null;
+        }
+
+        future = readFunction.apply(item);
+        cache.setLoadFuture(item.key(), future);
+        if (logger.isTraceEnabled())
+            logger.trace("Loading new item for {} while loading for {}. ({})", item.key(), callback, item);
+        return future;
+    }
+
+
+    private <K, V extends AccordState<K>> List<Future<?>> referenceAndDispatchReads(Iterable<K> keys,
+                                                                                           AccordStateCache.Instance<K, V> cache,
+                                                                                           Map<K, V> context,
+                                                                                           Function<V, Future<?>> readFunction,
+                                                                                           List<Future<?>> futures,
+                                                                                           Object callback)
+    {
+        for (K key : keys)
+        {
+            Future<?> future = referenceAndDispatch(key, cache, context, readFunction, callback);
+            if (future == null)
+                continue;
+
+            if (futures == null)
+                futures = new ArrayList<>();
+
+            futures.add(future);
+        }
+
+        return futures;
+    }
+
+    @VisibleForTesting
+    Function<AccordCommand, Future<?>> loadCommandFunction(Object callback)
+    {
+        return command -> Stage.READ.submit(() -> {
+            try
+            {
+                logger.trace("Starting load of {} for {}", command.txnId(), callback);
+                AccordKeyspace.loadCommand(command);
+                logger.trace("Completed load of {} for {}", command.txnId(), callback);
+            }
+            catch (Throwable t)
+            {
+                logger.error(String.format("Exception loading %s for %s", command.txnId(), callback), t);
+                throw t;
+            }
+        });
+    }
+
+    @VisibleForTesting
+    Function<AccordCommandsForKey, Future<?>> loadCommandsPerKeyFunction(Object callback)
+    {
+        return cfk -> Stage.READ.submit(() -> {
+            try
+            {
+                logger.trace("Starting load of {} for {}", cfk.key(), callback);
+                AccordKeyspace.loadCommandsForKey(cfk);
+                logger.trace("Completed load of {} for {}", cfk.key(), callback);
+            }
+            catch (Throwable t)
+            {
+                logger.error(String.format("Exception loading %s for %s", cfk.key(), callback), t);
+                throw t;
+            }
+        });
+    }
+
+    private Future<?> referenceAndDispatchReads(AsyncContext context, Object callback)
+    {
+        List<Future<?>> futures = null;
+
+        futures = referenceAndDispatchReads(txnIds,
+                                            commandStore.commandCache(),
+                                            context.commands.items,
+                                            loadCommandFunction(callback),
+                                            futures,
+                                            callback);
+
+        futures = referenceAndDispatchReads(keys,
+                                            commandStore.commandsForKeyCache(),
+                                            context.commandsForKey.items,
+                                            loadCommandsPerKeyFunction(callback),
+                                            futures,
+                                            callback);
+
+        return futures != null ? FutureCombiner.allOf(futures) : null;
+    }
+
+    @VisibleForTesting
+    void state(State state)
+    {
+        this.state = state;
+    }
+
+    public boolean load(AsyncContext context, BiConsumer<Object, Throwable> callback)
+    {
+        logger.trace("Running load for {} with state {}: {} {}", callback, state, txnIds, keys);
+        commandStore.checkInStoreThread();
+        switch (state)
+        {
+            case INITIALIZED:
+                state(State.SETUP);
+            case SETUP:
+                // notify any pending write only groups we're loading a full instance so the pending changes aren't removed
+                txnIds.forEach(commandStore.commandCache()::lockWriteOnlyGroupIfExists);
+                keys.forEach(commandStore.commandsForKeyCache()::lockWriteOnlyGroupIfExists);
+                readFuture = referenceAndDispatchReads(context, callback);
+                state(State.LOADING);
+            case LOADING:
+                if (readFuture != null)
+                {
+                    if (readFuture.isSuccess())
+                    {
+                        logger.trace("Read future succeeded for {}", callback);
+                        context.verifyLoaded();
+                        readFuture = null;
+                    }
+                    else
+                    {
+                        logger.trace("Adding callback for read future: {}", callback);
+                        readFuture.addCallback(callback, commandStore.executor());
+                        break;
+                    }
+                }
+                // apply any pending write only changes that may not have made it to disk in time to be loaded
+                context.commands.items.keySet().forEach(commandStore.commandCache()::cleanupLoadFuture);
+                context.commands.items.values().forEach(commandStore.commandCache()::applyAndRemoveWriteOnlyGroup);
+                context.commandsForKey.items.keySet().forEach(commandStore.commandsForKeyCache()::cleanupLoadFuture);
+                context.commandsForKey.items.values().forEach(commandStore.commandsForKeyCache()::applyAndRemoveWriteOnlyGroup);
+                // apply blindly reported timestamps
+                context.commandsForKey.items.values().forEach(AccordCommandsForKey::applyBlindWitnessedTimestamps);
+                state(State.FINISHED);
+            case FINISHED:
+                break;
+            default:
+                throw new IllegalStateException();
+        }
+
+        logger.trace("Exiting load for {} with state {}: {} {}", callback, state, txnIds, keys);
+        return state == State.FINISHED;
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/async/AsyncOperation.java b/src/java/org/apache/cassandra/service/accord/async/AsyncOperation.java
new file mode 100644
index 0000000000..ca2ea8617d
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/async/AsyncOperation.java
@@ -0,0 +1,253 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord.async;
+
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+
+import accord.api.Key;
+import accord.local.CommandStore;
+import accord.local.PreLoadContext;
+import accord.primitives.TxnId;
+import org.apache.cassandra.service.accord.AccordCommandStore;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.utils.concurrent.AsyncPromise;
+
+public abstract class AsyncOperation<R> extends AsyncPromise<R> implements Runnable, Function<CommandStore, R>, BiConsumer<Object, Throwable>
+{
+    private static final Logger logger = LoggerFactory.getLogger(AsyncOperation.class);
+
+    private static class LoggingProps
+    {
+        private static final String COMMAND_STORE = "command_store";
+        private static final String ASYNC_OPERATION = "async_op";
+    }
+
+    enum State
+    {
+        INITIALIZED,
+        LOADING,
+        RUNNING,
+        SAVING,
+        AWAITING_SAVE,
+        COMPLETING,
+        FINISHED,
+        FAILED
+    }
+
+    public interface Context
+    {
+
+    }
+
+    private State state = State.INITIALIZED;
+    private final AccordCommandStore commandStore;
+    private final AsyncLoader loader;
+    private final AsyncWriter writer;
+    private final AsyncContext context = new AsyncContext();
+    private R result;
+    private final String loggingId;
+
+    private void setLoggingIds()
+    {
+        MDC.put(LoggingProps.COMMAND_STORE, commandStore.loggingId);
+        MDC.put(LoggingProps.ASYNC_OPERATION, loggingId);
+    }
+
+    private void clearLoggingIds()
+    {
+        MDC.remove(LoggingProps.COMMAND_STORE);
+        MDC.remove(LoggingProps.ASYNC_OPERATION);
+    }
+
+    public AsyncOperation(AccordCommandStore commandStore, Iterable<TxnId> commandsToLoad, Iterable<PartitionKey> keyCommandsToLoad)
+    {
+        this.loggingId = "0x" + Integer.toHexString(System.identityHashCode(this));
+        this.commandStore = commandStore;
+        this.loader = createAsyncLoader(commandStore, commandsToLoad, keyCommandsToLoad);
+        setLoggingIds();
+        this.writer = createAsyncWriter(commandStore);
+        logger.trace("Created {} on {}", this, commandStore);
+        clearLoggingIds();
+    }
+
+    @Override
+    public String toString()
+    {
+        return "AsyncOperation{" + state + "}-0x" + Integer.toHexString(System.identityHashCode(this));
+    }
+
+    AsyncWriter createAsyncWriter(AccordCommandStore commandStore)
+    {
+        return new AsyncWriter(commandStore);
+    }
+
+    AsyncLoader createAsyncLoader(AccordCommandStore commandStore, Iterable<TxnId> txnIds, Iterable<PartitionKey> keys)
+    {
+        return new AsyncLoader(commandStore, txnIds, keys);
+    }
+
+    @VisibleForTesting
+    State state()
+    {
+        return state;
+    }
+
+    @VisibleForTesting
+    protected void setState(State state)
+    {
+        this.state = state;
+    }
+
+    /**
+     * callback for loader and writer
+     */
+    @Override
+    public void accept(Object o, Throwable throwable)
+    {
+        if (throwable != null)
+        {
+            logger.error(String.format("Operation %s failed", this), throwable);
+            state = State.FAILED;
+            tryFailure(throwable);
+        }
+        else
+            run();
+    }
+
+    protected void runInternal()
+    {
+        switch (state)
+        {
+            case INITIALIZED:
+                state = State.LOADING;
+            case LOADING:
+                if (!loader.load(context, this))
+                    return;
+
+                state = State.RUNNING;
+                result = apply(commandStore);
+
+                state = State.SAVING;
+            case SAVING:
+            case AWAITING_SAVE:
+                boolean updatesPersisted = writer.save(context, this);
+
+                if (state != State.AWAITING_SAVE)
+                {
+                    // with any updates on the way to disk, release resources so operations waiting
+                    // to use these objects don't have issues with fields marked as unsaved
+                    context.releaseResources(commandStore);
+                    state = State.AWAITING_SAVE;
+                }
+
+                if (!updatesPersisted)
+                    return;
+
+                state = State.COMPLETING;
+                setSuccess(result);
+                state = State.FINISHED;
+            case FINISHED:
+                break;
+            default:
+                throw new IllegalStateException();
+        }
+    }
+
+
+    @Override
+    public void run()
+    {
+        setLoggingIds();
+        logger.trace("Running {} with state {}", this, state);
+        commandStore.checkInStoreThread();
+        commandStore.setContext(context);
+        try
+        {
+            runInternal();
+        }
+        catch (Throwable t)
+        {
+            logger.error(String.format("Operation %s failed", this), t);
+            tryFailure(t);
+        }
+        finally
+        {
+            commandStore.unsetContext(context);
+            logger.trace("Exiting {}", this);
+            clearLoggingIds();
+        }
+    }
+
+    private static Iterable<PartitionKey> toPartitionKeys(Iterable<? extends Key> iterable)
+    {
+        return (Iterable<PartitionKey>) iterable;
+    }
+
+    static class ForFunction<R> extends AsyncOperation<R>
+    {
+        private final Function<? super CommandStore, R> function;
+
+        public ForFunction(AccordCommandStore commandStore, Iterable<TxnId> txnIds, Iterable<PartitionKey> keys, Function<? super CommandStore, R> function)
+        {
+            super(commandStore, txnIds, keys);
+            this.function = function;
+        }
+
+        @Override
+        public R apply(CommandStore commandStore)
+        {
+            return function.apply(commandStore);
+        }
+    }
+
+    public static <T> AsyncOperation<T> create(CommandStore commandStore, PreLoadContext loadCtx, Function<? super CommandStore, T> function)
+    {
+        return new ForFunction<>((AccordCommandStore) commandStore, loadCtx.txnIds(), AsyncOperation.toPartitionKeys(loadCtx.keys()), function);
+    }
+
+    static class ForConsumer  extends AsyncOperation<Void>
+    {
+        private final Consumer<? super CommandStore> consumer;
+
+        public ForConsumer(AccordCommandStore commandStore, Iterable<TxnId> txnIds, Iterable<PartitionKey> keys, Consumer<? super CommandStore> consumer)
+        {
+            super(commandStore, txnIds, keys);
+            this.consumer = consumer;
+        }
+
+        @Override
+        public Void apply(CommandStore commandStore)
+        {
+            consumer.accept(commandStore);
+            return null;
+        }
+    }
+
+    public static AsyncOperation<Void> create(CommandStore commandStore, PreLoadContext loadCtx, Consumer<? super CommandStore> consumer)
+    {
+        return new ForConsumer((AccordCommandStore) commandStore, loadCtx.txnIds(), AsyncOperation.toPartitionKeys(loadCtx.keys()), consumer);
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/async/AsyncWriter.java b/src/java/org/apache/cassandra/service/accord/async/AsyncWriter.java
new file mode 100644
index 0000000000..750aa93e12
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/async/AsyncWriter.java
@@ -0,0 +1,321 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord.async;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accord.api.Key;
+import accord.local.Status;
+import accord.primitives.TxnId;
+import org.apache.cassandra.concurrent.Stage;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.service.accord.AccordCommand;
+import org.apache.cassandra.service.accord.AccordCommandStore;
+import org.apache.cassandra.service.accord.AccordCommandsForKey;
+import org.apache.cassandra.service.accord.AccordKeyspace;
+import org.apache.cassandra.service.accord.AccordPartialCommand;
+import org.apache.cassandra.service.accord.AccordStateCache;
+import org.apache.cassandra.service.accord.AccordState;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.service.accord.store.StoredNavigableMap;
+import org.apache.cassandra.service.accord.store.StoredSet;
+import org.apache.cassandra.utils.concurrent.Future;
+import org.apache.cassandra.utils.concurrent.FutureCombiner;
+
+public class AsyncWriter
+{
+    private static final Logger logger = LoggerFactory.getLogger(AsyncWriter.class);
+
+    enum State
+    {
+        INITIALIZED,
+        SETUP,
+        SAVING,
+        FINISHED
+    }
+
+    private State state = State.INITIALIZED;
+    protected Future<?> writeFuture;
+    private final AccordCommandStore commandStore;
+    AccordStateCache.Instance<TxnId, AccordCommand> commandCache;
+    AccordStateCache.Instance<PartitionKey, AccordCommandsForKey> cfkCache;
+
+    public AsyncWriter(AccordCommandStore commandStore)
+    {
+        this.commandStore = commandStore;
+        this.commandCache = commandStore.commandCache();
+        this.cfkCache = commandStore.commandsForKeyCache();
+    }
+
+    private interface StateMutationFunction<K, V extends AccordState<K>>
+    {
+        Mutation apply(V state, long timestamp);
+    }
+
+    private static <K, V extends AccordState<K>> List<Future<?>> dispatchWrites(AsyncContext.Group<K, V> ctxGroup,
+                                                                                AccordStateCache.Instance<K, V> cache,
+                                                                                StateMutationFunction<K, V> mutationFunction,
+                                                                                long timestamp,
+                                                                                AccordCommandStore commandStore,
+                                                                                List<Future<?>> futures,
+                                                                                Object callback)
+    {
+        for (V item : ctxGroup.items.values())
+        {
+            if (!item.hasModifications())
+            {
+                if (logger.isTraceEnabled())
+                    logger.trace("No modifications for {} for {}, {}", item.key(), callback, item);
+                continue;
+            }
+
+            if (futures == null) futures = new ArrayList<>();
+            K key = item.key();
+            Mutation mutation = mutationFunction.apply(item, timestamp);
+            if (logger.isTraceEnabled())
+                logger.trace("Dispatching mutation for {} for {}, {} -> {}", key, callback, item, mutation);
+            Future<?> future = Stage.MUTATION.submit(() -> {
+                try
+                {
+                    if (logger.isTraceEnabled())
+                        logger.trace("Applying mutation for {} for {}: {}", key, callback, mutation);
+                    mutation.apply();
+                    if (logger.isTraceEnabled())
+                        logger.trace("Completed applying mutation for {} for {}: {}", key, callback, mutation);
+                }
+                catch (Throwable t)
+                {
+                    logger.error(String.format("Exception applying mutation for %s for %s: %s", key, callback, mutation), t);
+                    throw t;
+                }
+            });
+            cache.addSaveFuture(item.key(), future);
+            futures.add(future);
+        }
+
+        for (AccordState.WriteOnly<K, V> item : ctxGroup.writeOnly.values())
+        {
+            Preconditions.checkState(item.hasModifications());
+            if (futures == null) futures = new ArrayList<>();
+            Mutation mutation = mutationFunction.apply((V) item, timestamp);
+            Future<?> future = Stage.MUTATION.submit((Runnable) mutation::apply);
+            future.addListener(() -> cache.purgeWriteOnly(item.key()), commandStore.executor());
+            item.future(future);
+            futures.add(future);
+        }
+
+        return futures;
+    }
+
+    private Future<?> maybeDispatchWrites(AsyncContext context, Object callback) throws IOException
+    {
+        List<Future<?>> futures = null;
+
+        long timestamp = commandStore.nextSystemTimestampMicros();
+        futures = dispatchWrites(context.commands,
+                                 commandStore.commandCache(),
+                                 AccordKeyspace::getCommandMutation,
+                                 timestamp,
+                                 commandStore,
+                                 futures,
+                                 callback);
+
+        futures = dispatchWrites(context.commandsForKey,
+                                 commandStore.commandsForKeyCache(),
+                                 AccordKeyspace::getCommandsForKeyMutation,
+                                 timestamp,
+                                 commandStore,
+                                 futures,
+                                 callback);
+
+        return futures != null ? FutureCombiner.allOf(futures) : null;
+    }
+
+    private void denormalizeBlockedOn(AccordCommand command,
+                                      AsyncContext context,
+                                      Function<AccordCommand, StoredNavigableMap<TxnId, ByteBuffer>> waitingField,
+                                      Function<AccordCommand, StoredSet.Navigable<TxnId>> blockingField)
+    {
+        StoredNavigableMap<TxnId, ?> waitingOn = waitingField.apply(command);
+        waitingOn.forEachDeletion(deletedId -> {
+            AccordCommand blockedOn = commandForDenormalization(deletedId, context);
+            blockingField.apply(blockedOn).blindRemove(command.txnId());
+        });
+
+        waitingOn.forEachAddition((addedId, unused) -> {
+            AccordCommand blockedOn = commandForDenormalization(addedId, context);
+            blockingField.apply(blockedOn).blindAdd(command.txnId());
+        });
+    }
+
+    private void denormalizeWaitingOnSummaries(AccordCommand command,
+                                               AsyncContext context,
+                                               ByteBuffer summary,
+                                               Function<AccordCommand, StoredNavigableMap<TxnId, ByteBuffer>> waitingField,
+                                               Function<AccordCommand, StoredSet.Navigable<TxnId>> blockingField)
+    {
+        blockingField.apply(command).getView().forEach(blockingId -> {
+            AccordCommand blocking = commandForDenormalization(blockingId, context);
+            waitingField.apply(blocking).blindPut(command.txnId(), summary.duplicate());
+        });
+    }
+
+    private static <K, V extends AccordState<K>>
+            AccordState<K> getForDenormalization(K key,
+                                                 AccordCommandStore commandStore,
+                                                 AsyncContext.Group<K, V> ctxGroup,
+                                                 AccordStateCache.Instance<K, V> cache,
+                                                 BiFunction<AccordCommandStore, K, AccordState.WriteOnly<K, V>> factory)
+    {
+        V item = ctxGroup.get(key);
+        if (item != null)
+            return item;
+
+        item = cache.getOrNull(key);
+        if (item != null)
+        {
+            ctxGroup.items.put(key, item);
+            return item;
+        }
+
+        return ctxGroup.getOrCreateWriteOnly(key, factory, commandStore);
+    }
+
+    private AccordCommand commandForDenormalization(TxnId txnId, AsyncContext context)
+    {
+        return (AccordCommand) getForDenormalization(txnId, commandStore, context.commands, commandCache, AccordCommand.WriteOnly::new);
+    }
+
+    private AccordCommandsForKey cfkForDenormalization(PartitionKey key, AsyncContext context)
+    {
+        return (AccordCommandsForKey) getForDenormalization(key, commandStore, context.commandsForKey, cfkCache, AccordCommandsForKey.WriteOnly::new);
+    }
+
+    private void denormalize(AccordCommand command, AsyncContext context, Object callback)
+    {
+        if (!command.hasModifications())
+            return;
+
+        // notify commands we're waiting on that they need to update the summaries in our maps
+        if (command.waitingOnCommit.hasModifications())
+            denormalizeBlockedOn(command, context, cmd -> cmd.waitingOnCommit, cmd -> cmd.blockingCommitOn);
+        if (command.waitingOnApply.hasModifications())
+            denormalizeBlockedOn(command, context, cmd -> cmd.waitingOnApply, cmd -> cmd.blockingApplyOn);
+
+        if (command.shouldUpdateDenormalizedWaitingOn())
+        {
+            ByteBuffer summary = AccordPartialCommand.serializer.serialize(command);
+            denormalizeWaitingOnSummaries(command, context, summary, cmd -> cmd.waitingOnCommit, cmd -> cmd.blockingCommitOn);
+            denormalizeWaitingOnSummaries(command, context, summary, cmd -> cmd.waitingOnApply, cmd -> cmd.blockingApplyOn);
+        }
+
+        // There won't be a txn to denormalize against until the command has been preaccepted
+        if (command.status().hasBeen(Status.PreAccepted) && AccordPartialCommand.WithDeps.serializer.needsUpdate(command))
+        {
+            for (Key key : command.txn().keys())
+            {
+                PartitionKey partitionKey = (PartitionKey) key;
+                AccordCommandsForKey cfk = cfkForDenormalization(partitionKey, context);
+                cfk.updateSummaries(command);
+            }
+        }
+
+        if (logger.isTraceEnabled())
+        {
+            context.commands.items.forEach((txnId, cmd) -> logger.trace("Denormalized command {} for {}: {}", txnId, callback, cmd));
+            context.commandsForKey.items.forEach((key, cfk) -> logger.trace("Denormalized cfk {} for {}: {}", key, callback, cfk));
+        }
+    }
+
+    private void denormalizePartial(AccordPartialCommand partial, AsyncContext context, Object callback)
+    {
+        if (!partial.hasRemovedListeners())
+            return;
+
+        logger.trace("Denormalizing partial command change on {} for {}", partial, callback);
+        AccordCommand forUpdate = commandForDenormalization(partial.txnId(), context);
+        partial.forEachRemovedListener(forUpdate::removeListener);
+    }
+
+    private void denormalize(AsyncContext context, Object callback)
+    {
+        // need to clone "values" as denormalize will mutate it
+        new ArrayList<>(context.commands.items.values()).forEach(command -> denormalize(command, context, callback));
+        context.commands.partials.forEach(command -> denormalizePartial(command, context, callback));
+    }
+
+    @VisibleForTesting
+    void setState(State state)
+    {
+        this.state = state;
+    }
+
+    public boolean save(AsyncContext context, BiConsumer<Object, Throwable> callback)
+    {
+        logger.trace("Running save for {} with state {}", callback, state);
+        commandStore.checkInStoreThread();
+        try
+        {
+            switch (state)
+            {
+                case INITIALIZED:
+                    setState(State.SETUP);
+                case SETUP:
+                    denormalize(context, callback);
+                    writeFuture = maybeDispatchWrites(context, callback);
+
+                    setState(State.SAVING);
+                case SAVING:
+                    if (writeFuture != null && !writeFuture.isSuccess())
+                    {
+                        logger.trace("Adding callback for write future: {}", callback);
+                        writeFuture.addCallback(callback, commandStore.executor());
+                        break;
+                    }
+                    context.commands.items.keySet().forEach(commandStore.commandCache()::cleanupSaveFuture);
+                    context.commandsForKey.items.keySet().forEach(commandStore.commandsForKeyCache()::cleanupSaveFuture);
+                    setState(State.FINISHED);
+                case FINISHED:
+                    break;
+                default:
+                    throw new IllegalStateException();
+            }
+        }
+        catch (IOException e)
+        {
+            throw new RuntimeException(e);
+        }
+
+        logger.trace("Exiting save for {} with state {}", callback, state);
+        return state == State.FINISHED;
+    }
+
+}
diff --git a/src/java/org/apache/cassandra/service/accord/db/AbstractKeyIndexed.java b/src/java/org/apache/cassandra/service/accord/db/AbstractKeyIndexed.java
new file mode 100644
index 0000000000..84bb9cc216
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/db/AbstractKeyIndexed.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord.db;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Preconditions;
+
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.db.marshal.ByteBufferAccessor;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+/**
+ * Contains a map of objects serialized to byte buffers
+ * @param <T>
+ */
+public abstract class AbstractKeyIndexed<T>
+{
+    final NavigableMap<PartitionKey, ByteBuffer> serialized;
+
+    abstract void serialize(T t, DataOutputPlus out, int version) throws IOException;
+    abstract T deserialize(DataInputPlus in, int version) throws IOException;
+    abstract long serializedSize(T t, int version);
+    abstract long emptySizeOnHeap();
+
+    protected ByteBuffer serialize(T item)
+    {
+        int version = MessagingService.current_version;
+        long size = serializedSize(item, version) + TypeSizes.INT_SIZE;
+        try (DataOutputBuffer out = new DataOutputBuffer((int) size))
+        {
+            out.writeInt(version);
+            serialize(item, out, version);
+            return out.buffer(false);
+        }
+        catch (IOException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    protected T deserialize(ByteBuffer bytes)
+    {
+        try (DataInputBuffer in = new DataInputBuffer(bytes, true))
+        {
+            int version = in.readInt();
+            return deserialize(in, version);
+        }
+        catch (IOException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        AbstractKeyIndexed<?> that = (AbstractKeyIndexed<?>) o;
+        return serialized.equals(that.serialized);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash(serialized);
+    }
+
+    @Override
+    public String toString()
+    {
+        return getClass().getSimpleName() + serialized.entrySet().stream()
+                         .map(e -> e.getKey() + "=" + deserialize(e.getValue()))
+                         .collect(Collectors.joining(", ", "{", "}"));
+    }
+
+    public AbstractKeyIndexed(List<T> items, Function<T, PartitionKey> keyFunction)
+    {
+        this.serialized = new TreeMap<>();
+        for (int i=0, mi=items.size(); i<mi; i++)
+        {
+            T item = items.get(i);
+            PartitionKey key = keyFunction.apply(item);
+            // TODO: support multiple reads/writes per key
+            Preconditions.checkArgument(!this.serialized.containsKey(key));
+            this.serialized.put(key, serialize(item));
+        }
+    }
+
+    public AbstractKeyIndexed(NavigableMap<PartitionKey, ByteBuffer> serialized)
+    {
+        this.serialized = serialized;
+    }
+
+    public T getDeserialized(PartitionKey key)
+    {
+        ByteBuffer bytes = serialized.get(key);
+        if (bytes == null)
+            return null;
+        return deserialize(bytes);
+    }
+
+    public long estimatedSizeOnHeap()
+    {
+        long size = emptySizeOnHeap();
+        for (Map.Entry<PartitionKey, ByteBuffer> entry : serialized.entrySet())
+        {
+            size += entry.getKey().estimatedSizeOnHeap();
+            size += ByteBufferUtil.EMPTY_SIZE_ON_HEAP + ByteBufferAccessor.instance.size(entry.getValue());
+        }
+        return size;
+    }
+
+    static class Serializer<V, S extends AbstractKeyIndexed<V>> implements IVersionedSerializer<S>
+    {
+        private final Function<NavigableMap<PartitionKey, ByteBuffer>, S> factory;
+
+        Serializer(Function<NavigableMap<PartitionKey, ByteBuffer>, S> factory)
+        {
+            this.factory = factory;
+        }
+
+        @Override
+        public void serialize(S items, DataOutputPlus out, int version) throws IOException
+        {
+            out.writeUnsignedVInt(items.serialized.size());
+            for (Map.Entry<PartitionKey, ByteBuffer> entry : items.serialized.entrySet())
+            {
+                PartitionKey.serializer.serialize(entry.getKey(), out, version);
+                ByteBufferUtil.writeWithVIntLength(entry.getValue(), out);
+            }
+        }
+
+        @Override
+        public S deserialize(DataInputPlus in, int version) throws IOException
+        {
+            int size = (int) in.readUnsignedVInt();
+            NavigableMap<PartitionKey, ByteBuffer> items = new TreeMap<>();
+            for (int i=0; i<size; i++)
+                items.put(PartitionKey.serializer.deserialize(in, version), ByteBufferUtil.readWithVIntLength(in));
+            return factory.apply(items);
+        }
+
+        @Override
+        public long serializedSize(S items, int version)
+        {
+            long size = TypeSizes.sizeofUnsignedVInt(items.serialized.size());
+            for (Map.Entry<PartitionKey, ByteBuffer> entry : items.serialized.entrySet())
+            {
+                size += PartitionKey.serializer.serializedSize(entry.getKey());
+                size += ByteBufferUtil.serializedSizeWithVIntLength(entry.getValue());
+            }
+            return size;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/db/AccordData.java b/src/java/org/apache/cassandra/service/accord/db/AccordData.java
new file mode 100644
index 0000000000..40de220e7b
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/db/AccordData.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord.db;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+
+import com.google.common.base.Preconditions;
+
+import accord.api.Data;
+import accord.api.Result;
+import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.db.partitions.FilteredPartition;
+import org.apache.cassandra.db.rows.DeserializationHelper;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.db.rows.UnfilteredRowIteratorSerializer;
+import org.apache.cassandra.db.rows.UnfilteredRowIterators;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.schema.Schema;
+import org.apache.cassandra.schema.TableId;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.utils.ObjectSizes;
+
+public class AccordData extends AbstractKeyIndexed<FilteredPartition> implements Data, Result, Iterable<FilteredPartition>
+{
+    private static final long EMPTY_SIZE = ObjectSizes.measureDeep(new AccordData());
+
+    private static PartitionKey getKey(FilteredPartition partition)
+    {
+        return new PartitionKey(partition.metadata().id, partition.partitionKey());
+    }
+
+    public AccordData()
+    {
+        this(new TreeMap<>());
+    }
+
+    public AccordData(FilteredPartition partition)
+    {
+        this();
+        put(partition);
+    }
+
+    public AccordData(List<FilteredPartition> items)
+    {
+        super(items, AccordData::getKey);
+    }
+
+    public AccordData(NavigableMap<PartitionKey, ByteBuffer> serialized)
+    {
+        super(serialized);
+    }
+
+    @Override
+    void serialize(FilteredPartition partition, DataOutputPlus out, int version) throws IOException
+    {
+        partitionSerializer.serialize(partition, out, version);
+    }
+
+    @Override
+    FilteredPartition deserialize(DataInputPlus in, int version) throws IOException
+    {
+        return partitionSerializer.deserialize(in, version);
+    }
+
+    @Override
+    long serializedSize(FilteredPartition partition, int version)
+    {
+        return partitionSerializer.serializedSize(partition, version);
+    }
+
+    @Override
+    long emptySizeOnHeap()
+    {
+        return EMPTY_SIZE;
+    }
+
+    private void put(PartitionKey key, ByteBuffer bytes)
+    {
+        // TODO: support multiple partitions (ie: read commands) per partition
+        Preconditions.checkArgument(!serialized.containsKey(key) || serialized.get(key).equals(bytes));
+        serialized.put(key, bytes);
+    }
+
+    void put(PartitionKey key, FilteredPartition partition)
+    {
+        put(key, serialize(partition));
+    }
+
+    void put(FilteredPartition partition)
+    {
+        put(AccordKey.of(partition), partition);
+    }
+
+    FilteredPartition get(PartitionKey key)
+    {
+        return getDeserialized(key);
+    }
+
+    @Override
+    public Iterator<FilteredPartition> iterator()
+    {
+        return serialized.values().stream().map(this::deserialize).iterator();
+    }
+
+    @Override
+    public Data merge(Data data)
+    {
+        AccordData that = (AccordData) data;
+        AccordData merged = new AccordData();
+        //TODO on conflict should we "merge" the partition rather than override?
+        this.serialized.forEach(merged::put);
+        that.serialized.forEach(merged::put);
+        return merged;
+    }
+
+    public static Data merge(Data left, Data right)
+    {
+        if (left == null)
+            return right;
+        if (right == null)
+            return right;
+
+        return left.merge(right);
+    }
+
+    private static final IVersionedSerializer<FilteredPartition> partitionSerializer = new IVersionedSerializer<FilteredPartition>()
+    {
+        @Override
+        public void serialize(FilteredPartition partition, DataOutputPlus out, int version) throws IOException
+        {
+            partition.metadata().id.serialize(out);
+            TableMetadata metadata = Schema.instance.getTableMetadata(partition.metadata().id);
+            try (UnfilteredRowIterator iterator = partition.unfilteredIterator())
+            {
+                UnfilteredRowIteratorSerializer.serializer.serialize(iterator, ColumnFilter.all(metadata), out, version, partition.rowCount());
+            }
+        }
+
+        @Override
+        public FilteredPartition deserialize(DataInputPlus in, int version) throws IOException
+        {
+            TableMetadata metadata = Schema.instance.getTableMetadata(TableId.deserialize(in));
+            Preconditions.checkState(metadata != null);
+            try (UnfilteredRowIterator partition = UnfilteredRowIteratorSerializer.serializer.deserialize(in, version, metadata, ColumnFilter.all(metadata), DeserializationHelper.Flag.FROM_REMOTE))
+            {
+                return new FilteredPartition(UnfilteredRowIterators.filter(partition, 0));
+            }
+        }
+
+        @Override
+        public long serializedSize(FilteredPartition partition, int version)
+        {
+            long size = TableId.serializedSize();
+            TableMetadata metadata = Schema.instance.getTableMetadata(partition.metadata().id);
+            Preconditions.checkState(metadata != null);
+            try (UnfilteredRowIterator iterator = partition.unfilteredIterator())
+            {
+                return size + UnfilteredRowIteratorSerializer.serializer.serializedSize(iterator, ColumnFilter.all(metadata), version, partition.rowCount());
+            }
+        }
+    };
+
+    public static final IVersionedSerializer<AccordData> serializer = new Serializer<>(AccordData::new);
+}
diff --git a/src/java/org/apache/cassandra/service/accord/db/AccordQuery.java b/src/java/org/apache/cassandra/service/accord/db/AccordQuery.java
new file mode 100644
index 0000000000..fe79a353d3
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/db/AccordQuery.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord.db;
+
+import java.io.IOException;
+
+import javax.annotation.Nullable;
+
+import com.google.common.base.Preconditions;
+
+import accord.api.Data;
+import accord.api.Query;
+import accord.api.Read;
+import accord.api.Result;
+import accord.api.Update;
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.utils.ObjectSizes;
+
+public abstract class AccordQuery implements Query
+{
+    public static final AccordQuery ALL = new AccordQuery()
+    {
+        @Override
+        public Result compute(Data data, @Nullable Read read, @Nullable Update update)
+        {
+            return data != null ? (AccordData) data : new AccordData();
+        }
+    };
+
+    public static final AccordQuery NONE = new AccordQuery()
+    {
+        @Override
+        public Result compute(Data data, @Nullable Read read, @Nullable Update update)
+        {
+            return new AccordData();
+        }
+    };
+
+    private static final long SIZE = ObjectSizes.measure(ALL);
+
+    private AccordQuery() {}
+
+    public long estimatedSizeOnHeap()
+    {
+        return SIZE;
+    }
+
+    public static final IVersionedSerializer<AccordQuery> serializer = new IVersionedSerializer<AccordQuery>()
+    {
+        @Override
+        public void serialize(AccordQuery query, DataOutputPlus out, int version) throws IOException
+        {
+            Preconditions.checkArgument(query == ALL || query == NONE);
+            out.writeBoolean(query == ALL);
+        }
+
+        @Override
+        public AccordQuery deserialize(DataInputPlus in, int version) throws IOException
+        {
+            return in.readBoolean() ? ALL : NONE;
+        }
+
+        @Override
+        public long serializedSize(AccordQuery query, int version)
+        {
+            Preconditions.checkArgument(query == ALL || query == NONE);
+            return TypeSizes.sizeof(query == ALL);
+        }
+    };
+}
diff --git a/src/java/org/apache/cassandra/service/accord/db/AccordRead.java b/src/java/org/apache/cassandra/service/accord/db/AccordRead.java
new file mode 100644
index 0000000000..55fe14d665
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/db/AccordRead.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord.db;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import accord.api.Data;
+import accord.api.DataStore;
+import accord.api.Key;
+import accord.api.Read;
+import accord.local.CommandStore;
+import accord.primitives.Keys;
+import accord.primitives.Timestamp;
+import org.apache.cassandra.concurrent.Stage;
+import org.apache.cassandra.db.ReadExecutionController;
+import org.apache.cassandra.db.SinglePartitionReadCommand;
+import org.apache.cassandra.db.partitions.FilteredPartition;
+import org.apache.cassandra.db.partitions.PartitionIterator;
+import org.apache.cassandra.db.partitions.PartitionIterators;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.service.accord.AccordCommandsForKey;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.concurrent.Future;
+import org.apache.cassandra.utils.concurrent.ImmediateFuture;
+
+public class AccordRead extends AbstractKeyIndexed<SinglePartitionReadCommand> implements Read
+{
+    private static final long EMPTY_SIZE = ObjectSizes.measureDeep(new AccordRead(new TreeMap<>()));
+
+    public AccordRead(List<SinglePartitionReadCommand> items)
+    {
+        super(items, AccordKey::of);
+    }
+
+    public AccordRead(NavigableMap<PartitionKey, ByteBuffer> serialized)
+    {
+        super(serialized);
+    }
+
+    @Override
+    void serialize(SinglePartitionReadCommand command, DataOutputPlus out, int version) throws IOException
+    {
+        SinglePartitionReadCommand.serializer.serialize(command, out, version);
+    }
+
+    @Override
+    SinglePartitionReadCommand deserialize(DataInputPlus in, int version) throws IOException
+    {
+        return (SinglePartitionReadCommand) SinglePartitionReadCommand.serializer.deserialize(in, version);
+    }
+
+    @Override
+    long serializedSize(SinglePartitionReadCommand command, int version)
+    {
+        return SinglePartitionReadCommand.serializer.serializedSize(command, version);
+    }
+
+    @Override
+    long emptySizeOnHeap()
+    {
+        return EMPTY_SIZE;
+    }
+
+    @Override
+    public Keys keys()
+    {
+        return new Keys(serialized.keySet());
+    }
+
+    public String toString()
+    {
+        return "AccordRead{" + super.toString() + '}';
+    }
+
+    @Override
+    public Future<Data> read(Key key, boolean isForWriteTxn, CommandStore commandStore, Timestamp executeAt, DataStore store)
+    {
+        SinglePartitionReadCommand command = getDeserialized((PartitionKey) key);
+        if (command == null)
+            return ImmediateFuture.success(new AccordData());
+
+        AccordCommandsForKey cfk = (AccordCommandsForKey) commandStore.commandsForKey(key);
+        int nowInSeconds = cfk.nowInSecondsFor(executeAt, isForWriteTxn);
+        Future<Data> future = Stage.READ.submit(() -> {
+            SinglePartitionReadCommand read = command.withNowInSec(nowInSeconds);
+            try (ReadExecutionController controller = read.executionController();
+                 UnfilteredPartitionIterator partition = read.executeLocally(controller);
+                 PartitionIterator iterator = UnfilteredPartitionIterators.filter(partition, read.nowInSec()))
+            {
+                FilteredPartition filtered = FilteredPartition.create(PartitionIterators.getOnlyElement(iterator, read));
+                AccordData result = new AccordData(filtered);
+                return result;
+            }
+        });
+
+        return future;
+    }
+
+    public static AccordRead forCommands(Collection<SinglePartitionReadCommand> commands)
+    {
+        List<SinglePartitionReadCommand> reads = new ArrayList<>(commands);
+        reads.sort(Comparator.comparing(SinglePartitionReadCommand::partitionKey));
+        return new AccordRead(reads);
+    }
+
+    public static final IVersionedSerializer<AccordRead> serializer = new Serializer<>(AccordRead::new);
+}
diff --git a/src/java/org/apache/cassandra/service/accord/db/AccordUpdate.java b/src/java/org/apache/cassandra/service/accord/db/AccordUpdate.java
new file mode 100644
index 0000000000..7abaeb261b
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/db/AccordUpdate.java
@@ -0,0 +1,927 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord.db;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import accord.api.Data;
+import accord.api.Key;
+import accord.api.Update;
+import accord.api.Write;
+import accord.primitives.Keys;
+import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.Columns;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.RegularAndStaticColumns;
+import org.apache.cassandra.db.SinglePartitionReadCommand;
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.db.marshal.ByteBufferAccessor;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.partitions.FilteredPartition;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.rows.BTreeRow;
+import org.apache.cassandra.db.rows.BufferCell;
+import org.apache.cassandra.db.rows.Cell;
+import org.apache.cassandra.db.rows.DeserializationHelper;
+import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.Schema;
+import org.apache.cassandra.schema.TableId;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.service.accord.serializers.KeySerializers;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.ObjectSizes;
+
+import static org.apache.cassandra.utils.CollectionSerializer.deserializeMap;
+import static org.apache.cassandra.utils.CollectionSerializer.serializeMap;
+import static org.apache.cassandra.utils.CollectionSerializer.serializedSizeMap;
+
+public class AccordUpdate implements Update
+{
+    private static final long EMPTY_SIZE = ObjectSizes.measure(new AccordUpdate(Keys.EMPTY, (ByteBuffer[]) null, (ByteBuffer[]) null));;
+
+    private final Keys keys;
+    private final ByteBuffer[] updates;
+    private final ByteBuffer[] predicates;
+
+    public static abstract class UpdatePredicate
+    {
+        public enum Type
+        {
+            EXISTS("EXISTS"),
+            NOT_EXISTS("NOT EXISTS"),
+            EQUAL("=="),
+            NOT_EQUAL("!="),
+            GREATER_THAN(">"),
+            GREATER_THAN_OR_EQUAL(">="),
+            LESS_THAN("<"),
+            LESS_THAN_OR_EQUAL("<=");
+
+            final String symbol;
+
+            Type(String symbol)
+            {
+                this.symbol = symbol;
+            }
+        }
+
+        final Type type;
+        final TableMetadata table;
+        final DecoratedKey key;
+        final Clustering<?> clustering;
+        final PartitionKey accordKey;
+
+        public UpdatePredicate(Type type, TableMetadata table, DecoratedKey key, Clustering<?> clustering)
+        {
+            this.type = type;
+            this.table = table;
+            this.key = key;
+            this.clustering = clustering;
+            this.accordKey = new PartitionKey(table.id, key);
+        }
+
+        @Override
+        public boolean equals(Object o)
+        {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            UpdatePredicate predicate = (UpdatePredicate) o;
+            return type == predicate.type && table.equals(predicate.table) && key.equals(predicate.key) && clustering.equals(predicate.clustering);
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return Objects.hash(type, table, key, clustering);
+        }
+
+        @Override
+        public String toString()
+        {
+            return table.keyspace + '.' + table.name + " ["
+                   + key + ", " + clustering.toString(table) + "] " + type.symbol;
+        }
+
+        public PartitionKey partitionKey()
+        {
+            return accordKey;
+        }
+
+        public Type type()
+        {
+            return type;
+        }
+
+        public boolean applies(FilteredPartition partition)
+        {
+            return applies(partition == null ? null : partition.getRow(clustering));
+        }
+
+        public boolean supportedByRead(SinglePartitionReadCommand read)
+        {
+            return read.metadata().id.equals(table.id)
+                   && read.partitionKey().equals(key)
+                   && read.clusteringIndexFilter().selects(clustering);
+        }
+
+        protected abstract boolean applies(Row row);
+
+        protected abstract void serializeBody(DataOutputPlus out, int version) throws IOException;
+        protected abstract long serializedBodySize(int version);
+    }
+
+    public static class ExistsPredicate extends UpdatePredicate
+    {
+        private static final Set<Type> TYPES = ImmutableSet.of(Type.EXISTS, Type.NOT_EXISTS);
+
+        public ExistsPredicate(Type type, TableMetadata table, DecoratedKey key, Clustering<?> clustering)
+        {
+            super(type, table, key, clustering);
+            Preconditions.checkArgument(TYPES.contains(type));
+        }
+
+        @Override
+        protected boolean applies(Row row)
+        {
+            boolean exists = row != null && !row.isEmpty();
+            switch (type())
+            {
+                case EXISTS:
+                    return exists;
+                case NOT_EXISTS:
+                    return !exists;
+                default:
+                    throw new IllegalStateException();
+            }
+        }
+
+        @Override
+        protected void serializeBody(DataOutputPlus out, int version)
+        {
+
+        }
+
+        @Override
+        protected long serializedBodySize(int version)
+        {
+            return 0;
+        }
+
+        public static ExistsPredicate deserialize(UpdatePredicate.Type type, TableMetadata table, DecoratedKey key, Clustering<?> clustering, DataInputPlus in, int version)
+        {
+            Preconditions.checkArgument(TYPES.contains(type));
+            return new ExistsPredicate(type, table, key, clustering);
+        }
+    }
+
+    public static class ValuePredicate extends UpdatePredicate
+    {
+        private static final Set<Type> TYPES = ImmutableSet.of(Type.EQUAL,
+                                                               Type.NOT_EQUAL,
+                                                               Type.GREATER_THAN,
+                                                               Type.GREATER_THAN_OR_EQUAL,
+                                                               Type.LESS_THAN,
+                                                               Type.LESS_THAN_OR_EQUAL);
+
+        private final ColumnMetadata column;
+        // TODO: add cell path
+        private final ByteBuffer value;
+
+        public ValuePredicate(Type type, TableMetadata table, DecoratedKey key, Clustering<?> clustering,
+                              ColumnMetadata column, ByteBuffer value)
+        {
+            super(type, table, key, clustering);
+            Preconditions.checkArgument(TYPES.contains(type));
+            this.column = column;
+            this.value = value;
+        }
+
+        @Override
+        public boolean equals(Object o)
+        {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            if (!super.equals(o)) return false;
+            ValuePredicate that = (ValuePredicate) o;
+            return column.equals(that.column) && value.equals(that.value);
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return Objects.hash(super.hashCode(), column, value);
+        }
+
+        @Override
+        public String toString()
+        {
+            return table.keyspace + '.' + table.name + " ["
+                   + key + ", " + clustering.toString(table) + ", " + column.name.toString() + "] "
+                   + type.symbol + ' ' + column.type.getString(value);
+        }
+
+        private <T> int compare(Cell<T> cell)
+        {
+            return column.type.compare(cell.value(), cell.accessor(), value, ByteBufferAccessor.instance);
+        }
+
+        @Override
+        public boolean supportedByRead(SinglePartitionReadCommand read)
+        {
+            return super.supportedByRead(read) && read.columnFilter().fetches(column);
+        }
+
+        @Override
+        protected boolean applies(Row row)
+        {
+            if (row == null)
+                return false;
+            Cell<?> cell = row.getCell(column);
+            int cmp = compare(cell);
+            switch (type())
+            {
+                case EQUAL:
+                    return cmp == 0;
+                case NOT_EQUAL:
+                    return cmp != 0;
+                case GREATER_THAN:
+                    return cmp > 0;
+                case GREATER_THAN_OR_EQUAL:
+                    return cmp >= 0;
+                case LESS_THAN:
+                    return cmp < 0;
+                case LESS_THAN_OR_EQUAL:
+                    return cmp <= 0;
+                default:
+                    throw new IllegalStateException();
+            }
+        }
+
+        @Override
+        protected void serializeBody(DataOutputPlus out, int version) throws IOException
+        {
+            ByteBufferUtil.writeWithVIntLength(column.name.bytes, out);
+            ByteBufferUtil.writeWithVIntLength(value, out);
+        }
+
+        @Override
+        protected long serializedBodySize(int version)
+        {
+            return ByteBufferUtil.serializedSizeWithVIntLength(column.name.bytes)
+                 + ByteBufferUtil.serializedSizeWithVIntLength(value);
+        }
+
+        public static ValuePredicate deserialize(UpdatePredicate.Type type, TableMetadata table, DecoratedKey key, Clustering<?> clustering, DataInputPlus in, int version) throws IOException
+        {
+            Preconditions.checkArgument(TYPES.contains(type));
+            ColumnMetadata column = table.getColumn(ByteBufferUtil.readWithVIntLength(in));
+            ByteBuffer value = ByteBufferUtil.readWithVIntLength(in);
+            return new ValuePredicate(type, table, key, clustering, column, value);
+        }
+    }
+
+    public abstract static class AbstractUpdate
+    {
+        interface Serializer<T extends AbstractUpdate>
+        {
+            void serializeBody(T update, DataOutputPlus out, int version) throws IOException;
+            T deserializeBody(Kind kind, DataInputPlus in, int version) throws IOException;
+            long serializedBodySize(T update, int version);
+        }
+        enum Kind { SIMPLE, APPEND, INCREMENT }
+        public abstract Kind kind();
+        public abstract PartitionKey partitionKey();
+        public abstract PartitionUpdate apply(FilteredPartition partition);
+        public abstract Serializer serializer();
+
+        public static final IVersionedSerializer<AbstractUpdate> serializer = new IVersionedSerializer<AbstractUpdate>()
+        {
+            @Override
+            public void serialize(AbstractUpdate updater, DataOutputPlus out, int version) throws IOException
+            {
+                out.writeInt(updater.kind().ordinal());
+                updater.serializer().serializeBody(updater, out, version);
+            }
+
+            @Override
+            public AbstractUpdate deserialize(DataInputPlus in, int version) throws IOException
+            {
+                Kind kind = Kind.values()[in.readInt()];
+                switch (kind)
+                {
+                    case SIMPLE:
+                        return SimpleUpdate.serializer.deserializeBody(kind, in, version);
+                    case APPEND:
+                        return AppendingUpdate.serializer.deserializeBody(kind, in, version);
+                    case INCREMENT:
+                        return IncrementingUpdate.serializer.deserializeBody(kind, in, version);
+                    default: throw new IllegalArgumentException();
+                }
+            }
+
+            @Override
+            public long serializedSize(AbstractUpdate updater, int version)
+            {
+                return TypeSizes.INT_SIZE + updater.serializer().serializedBodySize(updater, version);
+            }
+        };
+    }
+
+    public static class SimpleUpdate extends AbstractUpdate
+    {
+        private final PartitionUpdate update;
+        private final PartitionKey accordKey;
+
+        public SimpleUpdate(PartitionUpdate update)
+        {
+            this.update = update;
+            this.accordKey = new PartitionKey(update.metadata().id, update.partitionKey());
+        }
+
+        @Override
+        public Kind kind()
+        {
+            return Kind.SIMPLE;
+        }
+
+        @Override
+        public PartitionKey partitionKey()
+        {
+            return accordKey;
+        }
+
+        @Override
+        public PartitionUpdate apply(FilteredPartition partition)
+        {
+            return update;
+        }
+
+        @Override
+        public Serializer serializer()
+        {
+            return serializer;
+        }
+
+        static final Serializer<SimpleUpdate> serializer = new Serializer<SimpleUpdate>()
+        {
+            @Override
+            public void serializeBody(SimpleUpdate update, DataOutputPlus out, int version) throws IOException
+            {
+                PartitionUpdate.serializer.serialize(update.update, out, version);
+            }
+
+            @Override
+            public SimpleUpdate deserializeBody(Kind kind, DataInputPlus in, int version) throws IOException
+            {
+                Preconditions.checkArgument(kind == Kind.SIMPLE);
+                return new SimpleUpdate(PartitionUpdate.serializer.deserialize(in, version, DeserializationHelper.Flag.FROM_REMOTE));
+            }
+
+            @Override
+            public long serializedBodySize(SimpleUpdate update, int version)
+            {
+                return PartitionUpdate.serializer.serializedSize(update.update, version);
+            }
+        };
+    }
+
+    private static final IVersionedSerializer<String> strSerializer = new IVersionedSerializer<String>()
+    {
+        @Override
+        public void serialize(String s, DataOutputPlus out, int version) throws IOException { out.writeUTF(s); }
+        @Override
+        public String deserialize(DataInputPlus in, int version) throws IOException { return in.readUTF(); }
+        @Override
+        public long serializedSize(String s, int version) { return TypeSizes.sizeof(s); }
+    };
+
+    private static final IVersionedSerializer<Integer> intSerializer = new IVersionedSerializer<Integer>()
+    {
+        @Override
+        public void serialize(Integer i, DataOutputPlus out, int version) throws IOException { out.writeInt(i); }
+
+        @Override
+        public Integer deserialize(DataInputPlus in, int version) throws IOException { return in.readInt(); }
+
+        @Override
+        public long serializedSize(Integer t, int version) { return TypeSizes.INT_SIZE; }
+    };
+
+    public static class AppendingUpdate extends AbstractUpdate
+    {
+        private final PartitionKey key;
+        private final Map<String, String> appends;
+
+        public AppendingUpdate(PartitionKey key, Map<String, String> appends)
+        {
+            Preconditions.checkArgument(appends != null && !appends.isEmpty());
+            TableMetadata metadata = Schema.instance.getTableMetadata(key.tableId());
+            Preconditions.checkArgument(metadata != null);
+            for (String column : appends.keySet())
+            {
+                ColumnMetadata cdef = metadata.getColumn(new ColumnIdentifier(column, true));
+                Preconditions.checkArgument(cdef != null);
+                Preconditions.checkArgument(cdef.type == UTF8Type.instance);
+                Preconditions.checkArgument(appends.get(column) != null);
+            }
+
+            this.key = key;
+            this.appends = appends;
+        }
+
+        @Override
+        public Kind kind()
+        {
+            return Kind.APPEND;
+        }
+
+        @Override
+        public PartitionKey partitionKey()
+        {
+            return key;
+        }
+
+        private static <T> String getString(Cell<T> cell)
+        {
+            return cell == null || cell.isTombstone() ? "" : UTF8Type.instance.getString(cell.value(), cell.accessor());
+        }
+
+        private static void apply(Row.Builder builder, Row current, ColumnMetadata cdef, String append)
+        {
+            Preconditions.checkArgument(append != null);
+            String value = getString(current.getCell(cdef)) + append;
+            builder.addCell(BufferCell.live(cdef, 0, UTF8Type.instance.fromString(value)));
+        }
+
+        private static Row apply(Row.Builder builder, Row current, Columns columns, Map<ColumnMetadata, String> appendMap)
+        {
+            if (columns.isEmpty())
+                return current;
+
+            builder.newRow(current.clustering());
+            for (ColumnMetadata column : columns)
+                apply(builder, current, column, appendMap.get(column));
+
+            return builder.build();
+        }
+
+        @Override
+        public PartitionUpdate apply(FilteredPartition partition)
+        {
+            Row.Builder rowBuilder = BTreeRow.unsortedBuilder();
+            TableMetadata metadata = Schema.instance.getTableMetadata(key.tableId());
+            RegularAndStaticColumns.Builder cbuilder = RegularAndStaticColumns.builder();
+            Map<ColumnMetadata, String> appendMap = new HashMap<>();
+            for (Map.Entry<String, String> entry : appends.entrySet())
+            {
+                ColumnMetadata cdef = metadata.getColumn(new ColumnIdentifier(entry.getKey(), true));
+                cbuilder.add(cdef);
+                appendMap.put(cdef, entry.getValue());
+            }
+
+            RegularAndStaticColumns columns = cbuilder.build();
+            Row staticRow = apply(rowBuilder, partition.staticRow(), columns.statics, appendMap);
+
+
+            Preconditions.checkArgument(metadata != null);
+            PartitionUpdate.Builder updateBuilder = new PartitionUpdate.Builder(metadata,
+                                                                                partition.partitionKey(),
+                                                                                columns,
+                                                                                staticRow,
+                                                                                partition.rowCount());
+            for (Row row : partition)
+                updateBuilder.add(apply(rowBuilder, row, columns.regulars, appendMap));
+
+            return updateBuilder.build();
+        }
+
+        @Override
+        public Serializer serializer()
+        {
+            return serializer;
+        }
+
+        static final Serializer<AppendingUpdate> serializer = new Serializer<AppendingUpdate>()
+        {
+            @Override
+            public void serializeBody(AppendingUpdate update, DataOutputPlus out, int version) throws IOException
+            {
+                PartitionKey.serializer.serialize(update.key, out, version);
+                serializeMap(strSerializer, strSerializer, update.appends, out, version);
+            }
+
+            @Override
+            public AppendingUpdate deserializeBody(Kind kind, DataInputPlus in, int version) throws IOException
+            {
+                Preconditions.checkArgument(kind == Kind.APPEND);
+                return new AppendingUpdate(PartitionKey.serializer.deserialize(in, version),
+                                           deserializeMap(strSerializer, strSerializer, Maps::newHashMapWithExpectedSize, in, version));
+            }
+
+            @Override
+            public long serializedBodySize(AppendingUpdate update, int version)
+            {
+                return PartitionKey.serializer.serializedSize(update.key) +
+                       serializedSizeMap(strSerializer, strSerializer, update.appends, version);
+            }
+        };
+    }
+
+    public static class IncrementingUpdate extends AbstractUpdate
+    {
+        private final PartitionKey key;
+        private final Map<String, Integer> increments;
+
+        public IncrementingUpdate(PartitionKey key, Map<String, Integer> increments)
+        {
+            Preconditions.checkArgument(increments != null && !increments.isEmpty());
+            TableMetadata metadata = Schema.instance.getTableMetadata(key.tableId());
+            Preconditions.checkArgument(metadata != null);
+
+            for (String column : increments.keySet())
+            {
+                ColumnMetadata cdef = metadata.getColumn(new ColumnIdentifier(column, true));
+                Preconditions.checkArgument(cdef != null);
+                Preconditions.checkArgument(cdef.type == Int32Type.instance);
+                Integer val = increments.get(column);
+                Preconditions.checkArgument(val != null && val != 0);
+            }
+            this.key = key;
+            this.increments = increments;
+        }
+
+        @Override
+        public Kind kind()
+        {
+            return Kind.INCREMENT;
+        }
+
+        @Override
+        public PartitionKey partitionKey()
+        {
+            return key;
+        }
+
+        private static <T> int getInt(Cell<T> cell)
+        {
+            return cell == null || cell.isTombstone() ? 0 : Int32Type.instance.compose(cell.value(), cell.accessor());
+        }
+
+        private static void apply(Row.Builder builder, Row current, ColumnMetadata cdef, int increment)
+        {
+            int value = getInt(current.getCell(cdef)) + increment;
+            builder.addCell(BufferCell.live(cdef, 0, Int32Type.instance.decompose(value)));
+        }
+
+        private static Row apply(Row.Builder builder, Row current, Columns columns, Map<ColumnMetadata, Integer> appendMap)
+        {
+            if (columns.isEmpty())
+                return current;
+
+            builder.newRow(current.clustering());
+            for (ColumnMetadata column : columns)
+                apply(builder, current, column, appendMap.get(column));
+
+            return builder.build();
+        }
+
+        @Override
+        public PartitionUpdate apply(FilteredPartition partition)
+        {
+            Row.Builder rowBuilder = BTreeRow.unsortedBuilder();
+            TableMetadata metadata = Schema.instance.getTableMetadata(key.tableId());
+            RegularAndStaticColumns.Builder cbuilder = RegularAndStaticColumns.builder();
+            Map<ColumnMetadata, Integer> incrementMap = new HashMap<>();
+            for (Map.Entry<String, Integer> entry : increments.entrySet())
+            {
+                ColumnMetadata cdef = metadata.getColumn(new ColumnIdentifier(entry.getKey(), true));
+                cbuilder.add(cdef);
+                incrementMap.put(cdef, entry.getValue());
+            }
+
+            RegularAndStaticColumns columns = cbuilder.build();
+            Row staticRow = apply(rowBuilder, partition.staticRow(), columns.statics, incrementMap);
+
+
+            Preconditions.checkArgument(metadata != null);
+            PartitionUpdate.Builder updateBuilder = new PartitionUpdate.Builder(metadata,
+                                                                                partition.partitionKey(),
+                                                                                columns,
+                                                                                staticRow,
+                                                                                partition.rowCount());
+            for (Row row : partition)
+                updateBuilder.add(apply(rowBuilder, row, columns.regulars, incrementMap));
+
+            return updateBuilder.build();
+        }
+
+        @Override
+        public Serializer serializer()
+        {
+            return serializer;
+        }
+
+        static final Serializer<IncrementingUpdate> serializer = new Serializer<IncrementingUpdate>()
+        {
+            @Override
+            public void serializeBody(IncrementingUpdate update, DataOutputPlus out, int version) throws IOException
+            {
+                PartitionKey.serializer.serialize(update.key, out, version);
+                serializeMap(strSerializer, intSerializer, update.increments, out, version);
+            }
+
+            @Override
+            public IncrementingUpdate deserializeBody(Kind kind, DataInputPlus in, int version) throws IOException
+            {
+                Preconditions.checkArgument(kind == Kind.INCREMENT);
+                return new IncrementingUpdate(PartitionKey.serializer.deserialize(in, version),
+                                           deserializeMap(strSerializer, intSerializer, Maps::newHashMapWithExpectedSize, in, version));
+            }
+
+            @Override
+            public long serializedBodySize(IncrementingUpdate update, int version)
+            {
+                return PartitionKey.serializer.serializedSize(update.key) +
+                       serializedSizeMap(strSerializer, intSerializer, update.increments, version);
+            }
+        };
+    }
+
+    private static Keys keysFrom(List<AbstractUpdate> updates, List<UpdatePredicate> predicates)
+    {
+        Set<Key> keys = new HashSet<>();
+        for (AbstractUpdate update : updates)
+            keys.add(update.partitionKey());
+        for (UpdatePredicate predicate : predicates)
+            keys.add(new PartitionKey(predicate.table.id, predicate.key));
+
+        return new Keys(keys);
+    }
+
+    public AccordUpdate(List<AbstractUpdate> updates, List<UpdatePredicate> predicates)
+    {
+        this.keys = keysFrom(updates, predicates);
+        this.updates = serialize(updates, AbstractUpdate.serializer);
+        this.predicates = serialize(predicates, predicateSerializer);
+    }
+
+    public AccordUpdate(Keys keys, ByteBuffer[] updates, ByteBuffer[] predicates)
+    {
+        this.keys = keys;
+        this.updates = updates;
+        this.predicates = predicates;
+    }
+
+    @Override
+    public String toString()
+    {
+        return "AccordUpdate{" +
+               "updates=" + Arrays.toString(updates) +
+               ", predicates=" + Arrays.toString(predicates) +
+               '}';
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        AccordUpdate update = (AccordUpdate) o;
+        return Arrays.equals(updates, update.updates) && Arrays.equals(predicates, update.predicates);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash(updates, predicates);
+    }
+
+    @Override
+    public Keys keys()
+    {
+        return keys;
+    }
+
+    @Override
+    public Write apply(Data data)
+    {
+        AccordData read = data != null ? (AccordData) data : new AccordData();
+        for (ByteBuffer bytes : predicates)
+        {
+            UpdatePredicate predicate = deserialize(bytes, predicateSerializer);
+            if (!predicate.applies(read.get(predicate.partitionKey())))
+                return AccordWrite.EMPTY;
+        }
+        NavigableMap<PartitionKey, PartitionUpdate> updateMap = new TreeMap<>();
+        for (AbstractUpdate updater : deserialize(updates, AbstractUpdate.serializer))
+        {
+            PartitionUpdate update = updater.apply(read.get(updater.partitionKey()));
+            PartitionKey key = AccordKey.of(update);
+            if (updateMap.containsKey(key))
+                update = PartitionUpdate.merge(Lists.newArrayList(updateMap.get(key), update));
+            updateMap.put(key, update);
+        }
+        return new AccordWrite(new ArrayList<>(updateMap.values()));
+    }
+
+    UpdatePredicate getPredicate(int i)
+    {
+        return deserialize(predicates[i], predicateSerializer);
+    }
+
+    public long estimatedSizeOnHeap()
+    {
+        long size = EMPTY_SIZE + ObjectSizes.sizeOfReferenceArray(updates.length) + ObjectSizes.sizeOfReferenceArray(predicates.length);
+        for (ByteBuffer buffer : updates)
+            size += ByteBufferUtil.estimatedSizeOnHeap(buffer);
+        for (ByteBuffer buffer : predicates)
+            size += ByteBufferUtil.estimatedSizeOnHeap(buffer);
+        return size;
+    }
+
+    static final IVersionedSerializer<UpdatePredicate> predicateSerializer = new IVersionedSerializer<UpdatePredicate>()
+    {
+        @Override
+        public void serialize(UpdatePredicate predicate, DataOutputPlus out, int version) throws IOException
+        {
+            out.writeInt(predicate.type.ordinal());
+            predicate.table.id.serialize(out);
+            ByteBufferUtil.writeWithVIntLength(predicate.key.getKey(), out);
+
+            Clustering.serializer.serialize(predicate.clustering, out, version, predicate.table.comparator.subtypes());
+
+            predicate.serializeBody(out, version);
+        }
+
+        @Override
+        public UpdatePredicate deserialize(DataInputPlus in, int version) throws IOException
+        {
+            UpdatePredicate.Type type = UpdatePredicate.Type.values()[in.readInt()];
+            TableId tableId = TableId.deserialize(in);
+            TableMetadata table = Schema.instance.getTableMetadata(tableId);
+            DecoratedKey key = table.partitioner.decorateKey(ByteBufferUtil.readWithVIntLength(in));
+            Clustering<?> clustering = Clustering.serializer.deserialize(in, version, table.comparator.subtypes());
+            switch (type)
+            {
+                case EXISTS:
+                case NOT_EXISTS:
+                    return ExistsPredicate.deserialize(type, table, key, clustering, in, version);
+                case EQUAL:
+                case NOT_EQUAL:
+                case GREATER_THAN:
+                case GREATER_THAN_OR_EQUAL:
+                case LESS_THAN:
+                case LESS_THAN_OR_EQUAL:
+                    return ValuePredicate.deserialize(type, table, key, clustering, in, version);
+                default:
+                    throw new IllegalArgumentException();
+            }
+        }
+
+        @Override
+        public long serializedSize(UpdatePredicate predicate, int version)
+        {
+            long size = TypeSizes.sizeof(predicate.type.ordinal());
+            size += predicate.table.id.serializedSize();
+            size += ByteBufferUtil.serializedSizeWithVIntLength(predicate.key.getKey());
+            size += Clustering.serializer.serializedSize(predicate.clustering, version, predicate.table.comparator.subtypes());
+            return size + predicate.serializedBodySize(version);
+        }
+    };
+
+    public static final IVersionedSerializer<AccordUpdate> serializer = new IVersionedSerializer<AccordUpdate>()
+    {
+        @Override
+        public void serialize(AccordUpdate update, DataOutputPlus out, int version) throws IOException
+        {
+            KeySerializers.keys.serialize(update.keys, out, version);
+            out.writeInt(update.updates.length);
+            for (ByteBuffer buffer : update.updates)
+                ByteBufferUtil.writeWithVIntLength(buffer, out);
+
+            out.writeInt(update.predicates.length);
+            for (ByteBuffer buffer : update.predicates)
+                ByteBufferUtil.writeWithVIntLength(buffer, out);
+        }
+
+        @Override
+        public AccordUpdate deserialize(DataInputPlus in, int version) throws IOException
+        {
+            Keys keys = KeySerializers.keys.deserialize(in, version);
+            int numUpdate = in.readInt();
+            ByteBuffer[] updates = new ByteBuffer[numUpdate];
+            for (int i=0; i<numUpdate; i++)
+                updates[i] = ByteBufferUtil.readWithVIntLength(in);
+
+            int numPredicate = in.readInt();
+            ByteBuffer[] predicates = new ByteBuffer[numPredicate];
+            for (int i=0; i<numPredicate; i++)
+                predicates[i] = ByteBufferUtil.readWithVIntLength(in);
+
+            return new AccordUpdate(keys, updates, predicates);
+        }
+
+        @Override
+        public long serializedSize(AccordUpdate update, int version)
+        {
+            long size = KeySerializers.keys.serializedSize(update.keys, version);
+
+            size += TypeSizes.sizeof(update.updates.length);
+            for (ByteBuffer buffer : update.updates)
+                size += ByteBufferUtil.serializedSizeWithVIntLength(buffer);
+
+            size += TypeSizes.sizeof(update.predicates.length);
+            for (ByteBuffer buffer : update.predicates)
+                size += ByteBufferUtil.serializedSizeWithVIntLength(buffer);
+
+            return size;
+        }
+    };
+
+    private static <T> ByteBuffer serialize(T item, IVersionedSerializer<T> serializer)
+    {
+        int version = MessagingService.current_version;
+        long size = serializer.serializedSize(item, version) + TypeSizes.INT_SIZE;
+        try (DataOutputBuffer out = new DataOutputBuffer((int) size))
+        {
+            out.writeInt(version);
+            serializer.serialize(item, out, version);
+            return out.buffer(false);
+        }
+        catch (IOException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static <T> ByteBuffer[] serialize(List<T> items, IVersionedSerializer<T> serializer)
+    {
+        ByteBuffer[] result = new ByteBuffer[items.size()];
+        for (int i=0,mi=items.size(); i<mi; i++)
+            result[i] = serialize(items.get(i), serializer);
+        return result;
+    }
+
+    private static <T> T deserialize(ByteBuffer bytes, IVersionedSerializer<T> serializer)
+    {
+        try (DataInputBuffer in = new DataInputBuffer(bytes, true))
+        {
+            int version = in.readInt();
+            return serializer.deserialize(in, version);
+        }
+        catch (IOException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static <T> List<T> deserialize(ByteBuffer[] buffers, IVersionedSerializer<T> serializer)
+    {
+        List<T> result = new ArrayList<>(buffers.length);
+        for (ByteBuffer bytes : buffers)
+            result.add(deserialize(bytes, serializer));
+        return result;
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/accord/db/AccordWrite.java b/src/java/org/apache/cassandra/service/accord/db/AccordWrite.java
new file mode 100644
index 0000000000..1b8526a4b7
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/db/AccordWrite.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord.db;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.NavigableMap;
+
+import com.google.common.collect.ImmutableList;
+
+import accord.api.DataStore;
+import accord.api.Key;
+import accord.api.Write;
+import accord.local.CommandStore;
+import accord.primitives.Timestamp;
+import org.apache.cassandra.concurrent.Stage;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.rows.DeserializationHelper;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.service.accord.AccordCommandsForKey;
+import org.apache.cassandra.service.accord.api.AccordKey;
+import org.apache.cassandra.service.accord.api.AccordKey.PartitionKey;
+import org.apache.cassandra.utils.ObjectSizes;
+import org.apache.cassandra.utils.concurrent.Future;
+import org.apache.cassandra.utils.concurrent.ImmediateFuture;
+
+public class AccordWrite extends AbstractKeyIndexed<PartitionUpdate> implements Write
+{
+    private static final Future<Void> SUCCESS = ImmediateFuture.success(null);
+    public static final AccordWrite EMPTY = new AccordWrite(ImmutableList.of());
+    private static final long EMPTY_SIZE = ObjectSizes.measureDeep(EMPTY);
+
+    public AccordWrite(List<PartitionUpdate> items)
+    {
+        super(items, AccordKey::of);
+    }
+
+    public AccordWrite(NavigableMap<PartitionKey, ByteBuffer> items)
+    {
+        super(items);
+    }
+
+    @Override
+    void serialize(PartitionUpdate update, DataOutputPlus out, int version) throws IOException
+    {
+        PartitionUpdate.serializer.serialize(update, out, version);
+    }
+
+    @Override
+    PartitionUpdate deserialize(DataInputPlus in, int version) throws IOException
+    {
+        return PartitionUpdate.serializer.deserialize(in, version, DeserializationHelper.Flag.FROM_REMOTE);
+    }
+
+    @Override
+    long serializedSize(PartitionUpdate update, int version)
+    {
+        return PartitionUpdate.serializer.serializedSize(update, version);
+    }
+
+    @Override
+    long emptySizeOnHeap()
+    {
+        return EMPTY_SIZE;
+    }
+
+    @Override
+    public Future<Void> apply(Key key, CommandStore commandStore, Timestamp executeAt, DataStore store)
+    {
+        PartitionUpdate update = getDeserialized((PartitionKey) key);
+        if (update == null)
+            return SUCCESS;
+        AccordCommandsForKey cfk = (AccordCommandsForKey) commandStore.commandsForKey(key);
+        long timestamp = cfk.timestampMicrosFor(executeAt, true);
+        int nowInSeconds = cfk.nowInSecondsFor(executeAt, true);
+        update = new PartitionUpdate.Builder(update, 0).updateAllTimestampAndLocalDeletionTime(timestamp, nowInSeconds).build();
+        Mutation mutation = new Mutation(update);
+        return (Future<Void>) Stage.MUTATION.submit((Runnable) mutation::apply);
+    }
+
+    public static final IVersionedSerializer<AccordWrite> serializer = new Serializer<>(AccordWrite::new);
+}
diff --git a/src/java/org/apache/cassandra/service/accord/serializers/AcceptSerializers.java b/src/java/org/apache/cassandra/service/accord/serializers/AcceptSerializers.java
new file mode 100644
index 0000000000..26a27ed69e
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/accord/serializers/AcceptSerializers.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.accord.serializers;
+
+import java.io.IOException;
+
+import accord.messages.Accept;
... 8274 lines suppressed ...


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org