Merge branch 'stable-2.16'

* stable-2.16:
  Skip checks in batch-update when there are no updates
  ZK compareAndPut should alway ignore the ignored refs
  Remove redundant ref-database migration flag
  Disable replication on startup
  Validate replication.config at Gerrit startup
  Write clear text data into ZK znodes
  Differentiate the multi-site Gson from Gerrit's
  Remove forwarded-aware event broker
  Revert "Remove the thread-local event forwarding logic"
  Use prim/backup logic for HAProxy reads
  Deploy multi-site plugin as gerrit lib module
  Ignore immutable and draft-comments Refs
  Remove hardcoded localhost from ZK test
  Wrap batch refupdate for shared refdb validation
  Always run in migration mode
  Fix NPE when creating new refs
  Bind GitRepositoryManager and associated factories
  Turn the multi-site plugin into a libModule
  Reformat with google-java-format
  Remove the thread-local event forwarding logic
  Remove plugin name from logs message
  Wrap Gerrit GitRepositoryManager into a Multisite fashion
  Wrap JGit Repository into a Multisite fashion
  Wrap RefDatabase into a MultiSite fashion
  Validate JGit RefUpdates events
  Add ref-database to local environment
  Remove redundant RefDatabaseConfig
  Always enable the shared ref-database
  Give proper name to the shared ref-database config
  Check for errors during downloading of plugins
  Removed unused obsolete constant
  Format config.md with wrapped lines and spaces
  Add migration mode to support missing entries
  Simplify the Zookeeper-based SharedDB code
  Implement Zookeeper backend for consistency check
  Fix directory creation in Docker test env

Requires Gerrit change I5b5a9432eb3db2559ce6a4393a42a4d227a371b5

Change-Id: Ic465e48a14376bb97e8ec10d69df66a5de87e201
diff --git a/BUILD b/BUILD
index 38da77a..c2c508b 100644
--- a/BUILD
+++ b/BUILD
@@ -19,6 +19,10 @@
     deps = [
         "@commons-lang3//jar",
         "@kafka_client//jar",
+        "@curator-framework//jar",
+        "@curator-recipes//jar",
+        "@curator-client//jar",
+        "@zookeeper//jar",
     ],
 )
 
@@ -46,5 +50,10 @@
         "@kafka_client//jar",
         "@testcontainers-kafka//jar",
         "//lib/testcontainers",
+        "@curator-framework//jar",
+        "@curator-recipes//jar",
+        "@curator-test//jar",
+        "@curator-client//jar",
+        "@zookeeper//jar",
     ],
 )
diff --git a/DESIGN.md b/DESIGN.md
index b466ba6..4b253f1 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -500,5 +500,3 @@
 - Serve RW/RW traffic based on the project name/ref-name.
 
 - Balance traffic with "locally-aware" policies based on historical data
-
-- Preventing split-brain in case of temporary sites isolation
diff --git a/README.md b/README.md
index 345fefb..fe8a86f 100644
--- a/README.md
+++ b/README.md
@@ -69,9 +69,16 @@
 
 ## How to configure
 
-Install the multi-site plugin into the `$GERRIT_SITE/plugins` directory of all
+Install the multi-site plugin into the `$GERRIT_SITE/lib` directory of all
 the Gerrit servers that are part of the multi-site cluster.
 
+Add the multi-site module to `$GERRIT_SITE/etc/gerrit.config` as follows:
+
+```
+[gerrit]
+  installModule = com.googlesource.gerrit.plugins.multisite.Module
+```
+
 Create the `$GERRIT_SITE/etc/multi-site.config` on all Gerrit servers with the
 following basic settings:
 
@@ -84,6 +91,12 @@
 
 [kafka "subscriber"]
   enabled = true
+
+[ref-database]
+  enabled = true
+
+[ref-database "zookeeper"]
+  connectString = "localhost:2181"
 ```
 
 For more details on the configuration settings, please refer to the
diff --git a/dockerised_local_env/Makefile b/dockerised_local_env/Makefile
index c76ab65..d4f5c39 100644
--- a/dockerised_local_env/Makefile
+++ b/dockerised_local_env/Makefile
@@ -11,37 +11,39 @@
 MYDIR=$(shell basename $(shell pwd))
 WGET=wget -N -q
 
-all: download build
+all: prepare download build
+
+prepare:
+	-mkdir -p $(GERRIT_1_PLUGINS_DIRECTORY) $(GERRIT_2_PLUGINS_DIRECTORY) $(GERRIT_1_BIN_DIRECTORY) $(GERRIT_2_BIN_DIRECTORY)
 
 download: gerrit plugin_websession_flatfile \
 	plugin_healthcheck \
 	plugin_multi_site
 
 
-gerrit:
-	-mkdir -p $(GERRIT_1_PLUGINS_DIRECTORY)
-	-mkdir -p $(GERRIT_2_PLUGINS_DIRECTORY)
+gerrit: prepare
 	$(WGET) $(CI_URL)/$(GERRIT_JOB)/lastSuccessfulBuild/artifact/gerrit/bazel-bin/release.war -P $(GERRIT_1_BIN_DIRECTORY)
 	cp $(GERRIT_1_BIN_DIRECTORY)/*.war $(GERRIT_2_BIN_DIRECTORY)
 	for plugin in $(CORE_PLUGINS); do $(WGET) $(CI_URL)/$(GERRIT_JOB)/$(BUILD_NUM)/artifact/gerrit/bazel-genfiles/plugins/$$plugin/$$plugin.jar -P $(GERRIT_1_PLUGINS_DIRECTORY); done
 	cp $(GERRIT_1_PLUGINS_DIRECTORY)/*.jar $(GERRIT_2_PLUGINS_DIRECTORY)
 
-plugin_websession_flatfile:
+plugin_websession_flatfile: prepare
 	$(WGET) $(CI_URL)/plugin-websession-flatfile-bazel-master-stable-2.16/lastSuccessfulBuild/artifact/bazel-genfiles/plugins/websession-flatfile/websession-flatfile.jar -P $(GERRIT_1_PLUGINS_DIRECTORY)
 	cp $(GERRIT_1_PLUGINS_DIRECTORY)/websession-flatfile.jar $(GERRIT_2_PLUGINS_DIRECTORY)/websession-flatfile.jar
 
-plugin_multi_site:
+plugin_multi_site: prepare
 	$(WGET) $(CI_URL)/plugin-multi-site-bazel-stable-2.16/lastSuccessfulBuild/artifact/bazel-genfiles/plugins/multi-site/multi-site.jar -P $(GERRIT_1_PLUGINS_DIRECTORY)
 	cp $(GERRIT_1_PLUGINS_DIRECTORY)/multi-site.jar $(GERRIT_2_PLUGINS_DIRECTORY)/multi-site.jar
 
-plugin_healthcheck:
+plugin_healthcheck: prepare
 	$(WGET) $(CI_URL)/plugin-healthcheck-bazel-stable-2.16/lastSuccessfulBuild/artifact/bazel-genfiles/plugins/healthcheck/healthcheck.jar -P $(GERRIT_1_PLUGINS_DIRECTORY)
 	cp $(GERRIT_1_PLUGINS_DIRECTORY)/healthcheck.jar $(GERRIT_2_PLUGINS_DIRECTORY)/healthcheck.jar
 
 build:
 	docker build -t $(MYDIR) ./gerrit-1
 	docker build -t $(MYDIR) ./gerrit-2
-clean_gerrit:
+
+clean_gerrit: prepare
 	rm -fr gerrit-1/db/ gerrit-1/data/ gerrit-1/cache/ gerrit-1/db/ gerrit-1/git/ gerrit-1/indexes/ gerrit-1/etc/
 	rm -fr gerrit-2/db/ gerrit-2/data/ gerrit-2/cache/ gerrit-2/db/ gerrit-2/git/ gerrit-2/indexes/ gerrit-1/etc/
 	-mkdir -p gerrit-{1,2}/etc/
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
index 678edd5..a8fabbb 100644
--- a/external_plugin_deps.bzl
+++ b/external_plugin_deps.bzl
@@ -19,6 +19,8 @@
     )
 
     BYTE_BUDDY_VER = "1.8.15"
+    CURATOR_VER = "4.2.0"
+    CURATOR_TEST_VER = "2.12.0"
 
     maven_jar(
         name = "byte_buddy",
@@ -55,3 +57,33 @@
         artifact = "org.apache.commons:commons-lang3:3.6",
         sha1 = "9d28a6b23650e8a7e9063c04588ace6cf7012c17",
     )
+
+    maven_jar(
+        name = "curator-test",
+        artifact = "org.apache.curator:curator-test:" + CURATOR_TEST_VER,
+        sha1 = "0a797be57ba95b67688a7615f7ad41ee6b3ceff0"
+    )
+
+    maven_jar(
+        name = "curator-framework",
+        artifact = "org.apache.curator:curator-framework:" + CURATOR_VER,
+        sha1 = "5b1cc87e17b8fe4219b057f6025662a693538861"
+    )
+
+    maven_jar(
+        name = "curator-recipes",
+        artifact = "org.apache.curator:curator-recipes:" + CURATOR_VER,
+        sha1 = "7f775be5a7062c2477c51533b9d008f70411ba8e"
+    )
+
+    maven_jar(
+        name = "curator-client",
+        artifact = "org.apache.curator:curator-client:" + CURATOR_VER,
+        sha1 = "d5d50930b8dd189f92c40258a6ba97675fea3e15"
+        )
+
+    maven_jar(
+        name = "zookeeper",
+        artifact = "org.apache.zookeeper:zookeeper:3.4.8",
+        sha1 = "933ea2ed15e6a0e24b788973e3d128ff163c3136"
+    )
diff --git a/setup_local_env/configs/gerrit.config b/setup_local_env/configs/gerrit.config
index 8666abe..e709162 100644
--- a/setup_local_env/configs/gerrit.config
+++ b/setup_local_env/configs/gerrit.config
@@ -2,6 +2,7 @@
     basePath = git
     serverId = 69ec38f0-350e-4d9c-96d4-bc956f2faaac
     canonicalWebUrl = $GERRIT_CANONICAL_WEB_URL
+    installModule = com.googlesource.gerrit.plugins.multisite.Module # multi-site needs to be a gerrit lib
 [database]
     type = h2
     database = $LOCATION_TEST_SITE/db/ReviewDB
diff --git a/setup_local_env/configs/multi-site.config b/setup_local_env/configs/multi-site.config
index f88c788..ed9e6ad 100644
--- a/setup_local_env/configs/multi-site.config
+++ b/setup_local_env/configs/multi-site.config
@@ -15,4 +15,6 @@
 	KafkaProp-autoCommitIntervalMs = 1000
 	KafkaProp-autoOffsetReset = latest
 [kafka "publisher"]
-	enabled = true
\ No newline at end of file
+	enabled = true
+[ref-database "zookeeper"]
+	connectString = localhost:$ZK_PORT
diff --git a/setup_local_env/configs/replication.config b/setup_local_env/configs/replication.config
index d866228..ea9252f 100644
--- a/setup_local_env/configs/replication.config
+++ b/setup_local_env/configs/replication.config
@@ -3,10 +3,10 @@
     push = +refs/*:refs/*
     timeout = 600
     rescheduleDelay = 15
-    replicationDelay = 5
+    replicationDelay = $REPLICATION_DELAY_SEC
 [gerrit]
     autoReload = true
-    replicateOnStartup = true
+    replicateOnStartup = false
 [replication]
     lockErrorMaxRetries = 5
     maxRetries = 5
\ No newline at end of file
diff --git a/setup_local_env/haproxy-config/haproxy.cfg b/setup_local_env/haproxy-config/haproxy.cfg
index 8afce07..94b22d8 100644
--- a/setup_local_env/haproxy-config/haproxy.cfg
+++ b/setup_local_env/haproxy-config/haproxy.cfg
@@ -44,7 +44,7 @@
     option httpchk GET /config/server/healthcheck~status HTTP/1.0
     http-check expect status 200
     server node1 $HA_GERRIT_SITE1_HOSTNAME:$HA_GERRIT_SITE1_HTTPD_PORT check inter 10s
-    server node2 $HA_GERRIT_SITE2_HOSTNAME:$HA_GERRIT_SITE2_HTTPD_PORT check inter 10s
+    server node2 $HA_GERRIT_SITE2_HOSTNAME:$HA_GERRIT_SITE2_HTTPD_PORT check inter 10s backup
 
 backend write-backendnodes
     mode http
diff --git a/setup_local_env/setup.sh b/setup_local_env/setup.sh
index ea18c66..177cd50 100755
--- a/setup_local_env/setup.sh
+++ b/setup_local_env/setup.sh
@@ -95,6 +95,9 @@
 	# KAFKA configuration
 	export KAFKA_PORT=9092
 
+	# ZK configuration
+	export ZK_PORT=2181
+
 	# SITE 1
 	GERRIT_SITE1_HOSTNAME=$1
 	GERRIT_SITE1_HTTPD_PORT=$2
@@ -140,7 +143,7 @@
 		echo "Usage: sh $0 [--option $value]"
 		echo
 		echo "[--release-war-file]            Location to release.war file"
-		echo "[--multisite-plugin-file]       Location to plugin multi-site.jar file"
+		echo "[--multisite-lib-file]          Location to lib multi-site.jar file"
 		echo
 		echo "[--new-deployment]              Cleans up previous gerrit deployment and re-installs it. default true"
 		echo "[--get-websession-plugin]       Download websession-flatfile plugin from CI lastSuccessfulBuild; default true"
@@ -159,6 +162,8 @@
 		echo
 		echo "[--replication-type]            Options [file,ssh]; default ssh"
 		echo "[--replication-ssh-user]        SSH user for the replication plugin; default $(whoami)"
+		echo "[--replication-delay]           Replication delay across the two instances in seconds"
+		echo
 		echo "[--just-cleanup-env]            Cleans up previous deployment; default false"
 		echo
 		echo "[--enabled-https]               Enabled https; default true"
@@ -185,8 +190,8 @@
 		shift
 		shift
   ;;
-  "--multisite-plugin-file" )
-		MULTISITE_PLUGIN_LOCATION=$2
+  "--multisite-lib-file" )
+		MULTISITE_LIB_LOCATION=$2
 		shift
 		shift
   ;;
@@ -235,6 +240,11 @@
 		shift
 		shift
   ;;
+  "--replication-delay")
+		export REPLICATION_DELAY_SEC=$2
+		shift
+		shift
+  ;;
   "--just-cleanup-env" )
        	JUST_CLEANUP_ENV=$2
 		shift
@@ -270,6 +280,7 @@
 GERRIT_2_SSHD_PORT=${GERRIT_2_SSHD_PORT:-"49418"}
 REPLICATION_TYPE=${REPLICATION_TYPE:-"ssh"}
 REPLICATION_SSH_USER=${REPLICATION_SSH_USER:-$(whoami)}
+export REPLICATION_DELAY_SEC=${REPLICATION_DELAY_SEC:-"5"}
 export SSH_ADVERTISED_PORT=${SSH_ADVERTISED_PORT:-"29418"}
 HTTPS_ENABLED=${HTTPS_ENABLED:-"true"}
 
@@ -280,7 +291,7 @@
 HA_PROXY_CERTIFICATES_DIR="$HA_PROXY_CONFIG_DIR/certificates"
 
 RELEASE_WAR_FILE_LOCATION=${RELEASE_WAR_FILE_LOCATION:-bazel-bin/release.war}
-MULTISITE_PLUGIN_LOCATION=${MULTISITE_PLUGIN_LOCATION:-bazel-genfiles/plugins/multi-site/multi-site.jar}
+MULTISITE_LIB_LOCATION=${MULTISITE_LIB_LOCATION:-bazel-genfiles/plugins/multi-site/multi-site.jar}
 
 
 export FAKE_NFS=$COMMON_LOCATION/fake_nfs
@@ -296,18 +307,20 @@
 else
 	cp -f $RELEASE_WAR_FILE_LOCATION $DEPLOYMENT_LOCATION/gerrit.war >/dev/null 2>&1 || { echo >&2 "$RELEASE_WAR_FILE_LOCATION: Not able to copy the file. Aborting"; exit 1; }
 fi
-if [ -z $MULTISITE_PLUGIN_LOCATION ];then
-	echo "The multi-site plugin is required. Usage: sh $0 --multisite-plugin-file /path/to/multi-site.jar"
+if [ -z $MULTISITE_LIB_LOCATION ];then
+	echo "The multi-site library is required. Usage: sh $0 --multisite-lib-file /path/to/multi-site.jar"
 	exit 1
 else
-	cp -f $MULTISITE_PLUGIN_LOCATION $DEPLOYMENT_LOCATION/multi-site.jar  >/dev/null 2>&1 || { echo >&2 "$MULTISITE_PLUGIN_LOCATION: Not able to copy the file. Aborting"; exit 1; }
+	cp -f $MULTISITE_LIB_LOCATION $DEPLOYMENT_LOCATION/multi-site.jar  >/dev/null 2>&1 || { echo >&2 "$MULTISITE_LIB_LOCATION: Not able to copy the file. Aborting"; exit 1; }
 fi
 if [ $DOWNLOAD_WEBSESSION_FLATFILE = "true" ];then
 	echo "Downloading websession-flatfile plugin stable 2.16"
 	wget https://gerrit-ci.gerritforge.com/view/Plugins-stable-2.16/job/plugin-websession-flatfile-bazel-master-stable-2.16/lastSuccessfulBuild/artifact/bazel-genfiles/plugins/websession-flatfile/websession-flatfile.jar \
-	-O $DEPLOYMENT_LOCATION/websession-flatfile.jar
+	-O $DEPLOYMENT_LOCATION/websession-flatfile.jar || { echo >&2 "Cannot download websession-flatfile plugin: Check internet connection. Abort\
+ing"; exit 1; }
 	wget https://gerrit-ci.gerritforge.com/view/Plugins-stable-2.16/job/plugin-healthcheck-bazel-stable-2.16/lastSuccessfulBuild/artifact/bazel-genfiles/plugins/healthcheck/healthcheck.jar \
-	-O $DEPLOYMENT_LOCATION/healthcheck.jar
+	-O $DEPLOYMENT_LOCATION/healthcheck.jar || { echo >&2 "Cannot download healthcheck plugin: Check internet connection. Abort\
+ing"; exit 1; }
 else
 	echo "Without the websession-flatfile; user login via haproxy will fail."
 fi
@@ -339,8 +352,8 @@
 	# Deploying TLS certificates
 	if [ "$HTTPS_ENABLED" = "true" ];then deploy_tls_certificates;fi
 
-	echo "Copy multi-site plugin"
-	cp -f $DEPLOYMENT_LOCATION/multi-site.jar $LOCATION_TEST_SITE_1/plugins/multi-site.jar
+	echo "Copy multi-site library"
+	cp -f $DEPLOYMENT_LOCATION/multi-site.jar $LOCATION_TEST_SITE_1/lib/multi-site.jar
 
 	echo "Copy websession-flatfile plugin"
 	cp -f $DEPLOYMENT_LOCATION/websession-flatfile.jar $LOCATION_TEST_SITE_1/plugins/websession-flatfile.jar
@@ -385,6 +398,7 @@
 echo "deployment-location=$DEPLOYMENT_LOCATION"
 echo "replication-type=$REPLICATION_TYPE"
 echo "replication-ssh-user=$REPLICATION_SSH_USER"
+echo "replication-delay=$REPLICATION_DELAY_SEC"
 echo "enable-https=$HTTPS_ENABLED"
 echo
 echo "GERRIT HA-PROXY: $GERRIT_CANONICAL_WEB_URL"
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
index acfaae2..58a11e9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
@@ -14,24 +14,39 @@
 
 package com.googlesource.gerrit.plugins.multisite;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Suppliers.memoize;
+import static com.google.common.base.Suppliers.ofInstance;
+
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.CaseFormat;
 import com.google.common.base.Strings;
+import com.google.common.base.Supplier;
 import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import com.google.inject.spi.Message;
 import com.googlesource.gerrit.plugins.multisite.forwarder.events.EventFamily;
+import java.io.IOException;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
 import java.util.UUID;
+import org.apache.commons.lang.StringUtils;
+import org.apache.curator.RetryPolicy;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.BoundedExponentialBackoffRetry;
 import org.apache.kafka.common.serialization.StringSerializer;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -39,6 +54,10 @@
 public class Configuration {
   private static final Logger log = LoggerFactory.getLogger(Configuration.class);
 
+  public static final String PLUGIN_NAME = "multi-site";
+  public static final String MULTI_SITE_CONFIG = PLUGIN_NAME + ".config";
+  public static final String REPLICATION_CONFIG = "replication.config";
+
   static final String INSTANCE_ID_FILE = "instanceId.data";
 
   // common parameters to cache and index sections
@@ -56,55 +75,113 @@
   static final String KAFKA_SECTION = "kafka";
   public static final String KAFKA_PROPERTY_PREFIX = "KafkaProp-";
 
-  private final KafkaPublisher publisher;
-  private final Cache cache;
-  private final Event event;
-  private final Index index;
-  private final KafkaSubscriber subscriber;
-  private final Kafka kafka;
+  private final Supplier<KafkaPublisher> publisher;
+  private final Supplier<Cache> cache;
+  private final Supplier<Event> event;
+  private final Supplier<Index> index;
+  private final Supplier<KafkaSubscriber> subscriber;
+  private final Supplier<Kafka> kafka;
+  private final Supplier<ZookeeperConfig> zookeeperConfig;
+  private final Supplier<Collection<Message>> replicationConfigValidation;
 
   @Inject
-  Configuration(PluginConfigFactory pluginConfigFactory, @PluginName String pluginName) {
-    this(pluginConfigFactory.getGlobalPluginConfig(pluginName));
+  Configuration(SitePaths sitePaths) {
+    this(getConfigFile(sitePaths, MULTI_SITE_CONFIG), getConfigFile(sitePaths, REPLICATION_CONFIG));
   }
 
   @VisibleForTesting
-  public Configuration(Config cfg) {
-    kafka = new Kafka(cfg);
-    publisher = new KafkaPublisher(cfg);
-    subscriber = new KafkaSubscriber(cfg);
-    cache = new Cache(cfg);
-    event = new Event(cfg);
-    index = new Index(cfg);
+  public Configuration(Config multiSiteConfig, Config replicationConfig) {
+    Supplier<Config> lazyCfg = lazyLoad(multiSiteConfig);
+    replicationConfigValidation = lazyValidateReplicatioConfig(replicationConfig);
+    kafka = memoize(() -> new Kafka(lazyCfg));
+    publisher = memoize(() -> new KafkaPublisher(lazyCfg));
+    subscriber = memoize(() -> new KafkaSubscriber(lazyCfg));
+    cache = memoize(() -> new Cache(lazyCfg));
+    event = memoize(() -> new Event(lazyCfg));
+    index = memoize(() -> new Index(lazyCfg));
+    zookeeperConfig = memoize(() -> new ZookeeperConfig(lazyCfg));
+  }
+
+  public ZookeeperConfig getZookeeperConfig() {
+    return zookeeperConfig.get();
   }
 
   public Kafka getKafka() {
-    return kafka;
+    return kafka.get();
   }
 
   public KafkaPublisher kafkaPublisher() {
-    return publisher;
+    return publisher.get();
   }
 
   public Cache cache() {
-    return cache;
+    return cache.get();
   }
 
   public Event event() {
-    return event;
+    return event.get();
   }
 
   public Index index() {
-    return index;
+    return index.get();
   }
 
   public KafkaSubscriber kafkaSubscriber() {
-    return subscriber;
+    return subscriber.get();
   }
 
-  private static int getInt(Config cfg, String section, String name, int defaultValue) {
+  public Collection<Message> validate() {
+    return replicationConfigValidation.get();
+  }
+
+  private static FileBasedConfig getConfigFile(SitePaths sitePaths, String configFileName) {
+    return new FileBasedConfig(sitePaths.etc_dir.resolve(configFileName).toFile(), FS.DETECTED);
+  }
+
+  private Supplier<Config> lazyLoad(Config config) {
+    if (config instanceof FileBasedConfig) {
+      return memoize(
+          () -> {
+            FileBasedConfig fileConfig = (FileBasedConfig) config;
+            String fileConfigFileName = fileConfig.getFile().getPath();
+            try {
+              log.info("Loading configuration from {}", fileConfigFileName);
+              fileConfig.load();
+            } catch (IOException | ConfigInvalidException e) {
+              log.error("Unable to load configuration from " + fileConfigFileName, e);
+            }
+            return fileConfig;
+          });
+    }
+    return ofInstance(config);
+  }
+
+  private Supplier<Collection<Message>> lazyValidateReplicatioConfig(Config replicationConfig) {
+    if (replicationConfig instanceof FileBasedConfig) {
+      FileBasedConfig fileConfig = (FileBasedConfig) replicationConfig;
+      try {
+        fileConfig.load();
+        return memoize(() -> validateReplicationConfig(replicationConfig));
+      } catch (IOException | ConfigInvalidException e) {
+        return ofInstance(Arrays.asList(new Message("Unable to load replication.config", e)));
+      }
+    }
+    return ofInstance(validateReplicationConfig(replicationConfig));
+  }
+
+  private Collection<Message> validateReplicationConfig(Config replicationConfig) {
+    if (replicationConfig.getBoolean("gerrit", "replicateOnStartup", false)) {
+      return Arrays.asList(
+          new Message(
+              "Invalid replication.config: gerrit.replicateOnStartup has to be set to 'false' for multi-site setups"));
+    }
+    return Collections.emptyList();
+  }
+
+  private static int getInt(
+      Supplier<Config> cfg, String section, String subSection, String name, int defaultValue) {
     try {
-      return cfg.getInt(section, name, defaultValue);
+      return cfg.get().getInt(section, subSection, name, defaultValue);
     } catch (IllegalArgumentException e) {
       log.error("invalid value for {}; using default value {}", name, defaultValue);
       log.debug("Failed to retrieve integer value: {}", e.getMessage(), e);
@@ -113,28 +190,32 @@
   }
 
   private static String getString(
-      Config cfg, String section, String subsection, String name, String defaultValue) {
-    String value = cfg.getString(section, subsection, name);
+      Supplier<Config> cfg, String section, String subsection, String name, String defaultValue) {
+    String value = cfg.get().getString(section, subsection, name);
     if (!Strings.isNullOrEmpty(value)) {
       return value;
     }
     return defaultValue;
   }
 
-  private static Map<EventFamily, Boolean> eventsEnabled(Config config, String subsection) {
+  private static Map<EventFamily, Boolean> eventsEnabled(
+      Supplier<Config> config, String subsection) {
     Map<EventFamily, Boolean> eventsEnabled = new HashMap<>();
     for (EventFamily eventFamily : EventFamily.values()) {
       String enabledConfigKey = eventFamily.lowerCamelName() + "Enabled";
 
       eventsEnabled.put(
           eventFamily,
-          config.getBoolean(
-              KAFKA_SECTION, subsection, enabledConfigKey, DEFAULT_ENABLE_PROCESSING));
+          config
+              .get()
+              .getBoolean(KAFKA_SECTION, subsection, enabledConfigKey, DEFAULT_ENABLE_PROCESSING));
     }
     return eventsEnabled;
   }
 
-  private static void applyKafkaConfig(Config config, String subsectionName, Properties target) {
+  private static void applyKafkaConfig(
+      Supplier<Config> configSupplier, String subsectionName, Properties target) {
+    Config config = configSupplier.get();
     for (String section : config.getSubsections(KAFKA_SECTION)) {
       if (section.equals(subsectionName)) {
         for (String name : config.getNames(KAFKA_SECTION, section, true)) {
@@ -154,7 +235,11 @@
     target.put(
         "bootstrap.servers",
         getString(
-            config, KAFKA_SECTION, null, "bootstrapServers", DEFAULT_KAFKA_BOOTSTRAP_SERVERS));
+            configSupplier,
+            KAFKA_SECTION,
+            null,
+            "bootstrapServers",
+            DEFAULT_KAFKA_BOOTSTRAP_SERVERS));
   }
 
   public static class Kafka {
@@ -163,12 +248,16 @@
 
     private static final Map<EventFamily, String> EVENT_TOPICS =
         ImmutableMap.of(
-            EventFamily.INDEX_EVENT, "GERRIT.EVENT.INDEX",
-            EventFamily.STREAM_EVENT, "GERRIT.EVENT.STREAM",
-            EventFamily.CACHE_EVENT, "GERRIT.EVENT.CACHE",
-            EventFamily.PROJECT_LIST_EVENT, "GERRIT.EVENT.PROJECT.LIST");
+            EventFamily.INDEX_EVENT,
+            "GERRIT.EVENT.INDEX",
+            EventFamily.STREAM_EVENT,
+            "GERRIT.EVENT.STREAM",
+            EventFamily.CACHE_EVENT,
+            "GERRIT.EVENT.CACHE",
+            EventFamily.PROJECT_LIST_EVENT,
+            "GERRIT.EVENT.PROJECT.LIST");
 
-    Kafka(Config config) {
+    Kafka(Supplier<Config> config) {
       this.bootstrapServers =
           getString(
               config, KAFKA_SECTION, null, "bootstrapServers", DEFAULT_KAFKA_BOOTSTRAP_SERVERS);
@@ -202,10 +291,11 @@
     private final boolean enabled;
     private final Map<EventFamily, Boolean> eventsEnabled;
 
-    private KafkaPublisher(Config cfg) {
+    private KafkaPublisher(Supplier<Config> cfg) {
       enabled =
-          cfg.getBoolean(
-              KAFKA_SECTION, KAFKA_PUBLISHER_SUBSECTION, ENABLE_KEY, DEFAULT_BROKER_ENABLED);
+          cfg.get()
+              .getBoolean(
+                  KAFKA_SECTION, KAFKA_PUBLISHER_SUBSECTION, ENABLE_KEY, DEFAULT_BROKER_ENABLED);
 
       eventsEnabled = eventsEnabled(cfg, KAFKA_PUBLISHER_SUBSECTION);
 
@@ -245,21 +335,22 @@
     private Map<EventFamily, Boolean> eventsEnabled;
     private final Config cfg;
 
-    public KafkaSubscriber(Config cfg) {
+    public KafkaSubscriber(Supplier<Config> configSupplier) {
+      this.cfg = configSupplier.get();
+
       this.pollingInterval =
           cfg.getInt(
               KAFKA_SECTION,
               KAFKA_SUBSCRIBER_SUBSECTION,
               "pollingIntervalMs",
               DEFAULT_POLLING_INTERVAL_MS);
-      this.cfg = cfg;
 
       enabled = cfg.getBoolean(KAFKA_SECTION, KAFKA_SUBSCRIBER_SUBSECTION, ENABLE_KEY, false);
 
-      eventsEnabled = eventsEnabled(cfg, KAFKA_SUBSCRIBER_SUBSECTION);
+      eventsEnabled = eventsEnabled(configSupplier, KAFKA_SUBSCRIBER_SUBSECTION);
 
       if (enabled) {
-        applyKafkaConfig(cfg, KAFKA_SUBSCRIBER_SUBSECTION, this);
+        applyKafkaConfig(configSupplier, KAFKA_SUBSCRIBER_SUBSECTION, this);
       }
     }
 
@@ -301,14 +392,14 @@
 
     private final boolean synchronize;
 
-    private Forwarding(Config cfg, String section) {
+    private Forwarding(Supplier<Config> cfg, String section) {
       synchronize = getBoolean(cfg, section, SYNCHRONIZE_KEY, DEFAULT_SYNCHRONIZE);
     }
 
     private static boolean getBoolean(
-        Config cfg, String section, String name, boolean defaultValue) {
+        Supplier<Config> cfg, String section, String name, boolean defaultValue) {
       try {
-        return cfg.getBoolean(section, name, defaultValue);
+        return cfg.get().getBoolean(section, name, defaultValue);
       } catch (IllegalArgumentException e) {
         log.error("invalid value for {}; using default value {}", name, defaultValue);
         log.debug("Failed to retrieve boolean value: {}", e.getMessage(), e);
@@ -328,10 +419,11 @@
     private final int threadPoolSize;
     private final List<String> patterns;
 
-    private Cache(Config cfg) {
+    private Cache(Supplier<Config> cfg) {
       super(cfg, CACHE_SECTION);
-      threadPoolSize = getInt(cfg, CACHE_SECTION, THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE);
-      patterns = Arrays.asList(cfg.getStringList(CACHE_SECTION, null, PATTERN_KEY));
+      threadPoolSize =
+          getInt(cfg, CACHE_SECTION, null, THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE);
+      patterns = Arrays.asList(cfg.get().getStringList(CACHE_SECTION, null, PATTERN_KEY));
     }
 
     public int threadPoolSize() {
@@ -346,7 +438,7 @@
   public static class Event extends Forwarding {
     static final String EVENT_SECTION = "event";
 
-    private Event(Config cfg) {
+    private Event(Supplier<Config> cfg) {
       super(cfg, EVENT_SECTION);
     }
   }
@@ -362,12 +454,15 @@
 
     private final int numStripedLocks;
 
-    private Index(Config cfg) {
+    private Index(Supplier<Config> cfg) {
       super(cfg, INDEX_SECTION);
-      threadPoolSize = getInt(cfg, INDEX_SECTION, THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE);
-      retryInterval = getInt(cfg, INDEX_SECTION, RETRY_INTERVAL_KEY, DEFAULT_INDEX_RETRY_INTERVAL);
-      maxTries = getInt(cfg, INDEX_SECTION, MAX_TRIES_KEY, DEFAULT_INDEX_MAX_TRIES);
-      numStripedLocks = getInt(cfg, INDEX_SECTION, NUM_STRIPED_LOCKS, DEFAULT_NUM_STRIPED_LOCKS);
+      threadPoolSize =
+          getInt(cfg, INDEX_SECTION, null, THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE);
+      retryInterval =
+          getInt(cfg, INDEX_SECTION, null, RETRY_INTERVAL_KEY, DEFAULT_INDEX_RETRY_INTERVAL);
+      maxTries = getInt(cfg, INDEX_SECTION, null, MAX_TRIES_KEY, DEFAULT_INDEX_MAX_TRIES);
+      numStripedLocks =
+          getInt(cfg, INDEX_SECTION, null, NUM_STRIPED_LOCKS, DEFAULT_NUM_STRIPED_LOCKS);
     }
 
     public int threadPoolSize() {
@@ -386,4 +481,132 @@
       return numStripedLocks;
     }
   }
+
+  public static class ZookeeperConfig {
+    public static final String SECTION = "ref-database";
+    public static final int defaultSessionTimeoutMs;
+    public static final int defaultConnectionTimeoutMs;
+    public static final String DEFAULT_ZK_CONNECT = "localhost:2181";
+    private final int DEFAULT_RETRY_POLICY_BASE_SLEEP_TIME_MS = 1000;
+    private final int DEFAULT_RETRY_POLICY_MAX_SLEEP_TIME_MS = 3000;
+    private final int DEFAULT_RETRY_POLICY_MAX_RETRIES = 3;
+    private final int DEFAULT_CAS_RETRY_POLICY_BASE_SLEEP_TIME_MS = 100;
+    private final int DEFAULT_CAS_RETRY_POLICY_MAX_SLEEP_TIME_MS = 300;
+    private final int DEFAULT_CAS_RETRY_POLICY_MAX_RETRIES = 3;
+
+    static {
+      CuratorFrameworkFactory.Builder b = CuratorFrameworkFactory.builder();
+      defaultSessionTimeoutMs = b.getSessionTimeoutMs();
+      defaultConnectionTimeoutMs = b.getConnectionTimeoutMs();
+    }
+
+    public static final String SUBSECTION = "zookeeper";
+    public static final String KEY_CONNECT_STRING = "connectString";
+    public static final String KEY_SESSION_TIMEOUT_MS = "sessionTimeoutMs";
+    public static final String KEY_CONNECTION_TIMEOUT_MS = "connectionTimeoutMs";
+    public static final String KEY_RETRY_POLICY_BASE_SLEEP_TIME_MS = "retryPolicyBaseSleepTimeMs";
+    public static final String KEY_RETRY_POLICY_MAX_SLEEP_TIME_MS = "retryPolicyMaxSleepTimeMs";
+    public static final String KEY_RETRY_POLICY_MAX_RETRIES = "retryPolicyMaxRetries";
+    public static final String KEY_LOCK_TIMEOUT_MS = "lockTimeoutMs";
+    public static final String KEY_ROOT_NODE = "rootNode";
+    public final String KEY_CAS_RETRY_POLICY_BASE_SLEEP_TIME_MS = "casRetryPolicyBaseSleepTimeMs";
+    public final String KEY_CAS_RETRY_POLICY_MAX_SLEEP_TIME_MS = "casRetryPolicyMaxSleepTimeMs";
+    public final String KEY_CAS_RETRY_POLICY_MAX_RETRIES = "casRetryPolicyMaxRetries";
+    public static final String KEY_MIGRATE = "migrate";
+
+    private final String connectionString;
+    private final String root;
+    private final int sessionTimeoutMs;
+    private final int connectionTimeoutMs;
+    private final int baseSleepTimeMs;
+    private final int maxSleepTimeMs;
+    private final int maxRetries;
+    private final int casBaseSleepTimeMs;
+    private final int casMaxSleepTimeMs;
+    private final int casMaxRetries;
+
+    private CuratorFramework build;
+
+    private ZookeeperConfig(Supplier<Config> cfg) {
+      connectionString =
+          getString(cfg, SECTION, SUBSECTION, KEY_CONNECT_STRING, DEFAULT_ZK_CONNECT);
+      root = getString(cfg, SECTION, SUBSECTION, KEY_ROOT_NODE, "gerrit/multi-site");
+      sessionTimeoutMs =
+          getInt(cfg, SECTION, SUBSECTION, KEY_SESSION_TIMEOUT_MS, defaultSessionTimeoutMs);
+      connectionTimeoutMs =
+          getInt(cfg, SECTION, SUBSECTION, KEY_CONNECTION_TIMEOUT_MS, defaultConnectionTimeoutMs);
+
+      baseSleepTimeMs =
+          getInt(
+              cfg,
+              SECTION,
+              SUBSECTION,
+              KEY_RETRY_POLICY_BASE_SLEEP_TIME_MS,
+              DEFAULT_RETRY_POLICY_BASE_SLEEP_TIME_MS);
+
+      maxSleepTimeMs =
+          getInt(
+              cfg,
+              SECTION,
+              SUBSECTION,
+              KEY_RETRY_POLICY_MAX_SLEEP_TIME_MS,
+              DEFAULT_RETRY_POLICY_MAX_SLEEP_TIME_MS);
+
+      maxRetries =
+          getInt(
+              cfg,
+              SECTION,
+              SUBSECTION,
+              KEY_RETRY_POLICY_MAX_RETRIES,
+              DEFAULT_RETRY_POLICY_MAX_RETRIES);
+
+      casBaseSleepTimeMs =
+          getInt(
+              cfg,
+              SECTION,
+              SUBSECTION,
+              KEY_CAS_RETRY_POLICY_BASE_SLEEP_TIME_MS,
+              DEFAULT_CAS_RETRY_POLICY_BASE_SLEEP_TIME_MS);
+
+      casMaxSleepTimeMs =
+          getInt(
+              cfg,
+              SECTION,
+              SUBSECTION,
+              KEY_CAS_RETRY_POLICY_MAX_SLEEP_TIME_MS,
+              DEFAULT_CAS_RETRY_POLICY_MAX_SLEEP_TIME_MS);
+
+      casMaxRetries =
+          getInt(
+              cfg,
+              SECTION,
+              SUBSECTION,
+              KEY_CAS_RETRY_POLICY_MAX_RETRIES,
+              DEFAULT_CAS_RETRY_POLICY_MAX_RETRIES);
+
+      checkArgument(StringUtils.isNotEmpty(connectionString), "zookeeper.%s contains no servers");
+    }
+
+    public CuratorFramework buildCurator() {
+      if (build == null) {
+        this.build =
+            CuratorFrameworkFactory.builder()
+                .connectString(connectionString)
+                .sessionTimeoutMs(sessionTimeoutMs)
+                .connectionTimeoutMs(connectionTimeoutMs)
+                .retryPolicy(
+                    new BoundedExponentialBackoffRetry(baseSleepTimeMs, maxSleepTimeMs, maxRetries))
+                .namespace(root)
+                .build();
+        this.build.start();
+      }
+
+      return this.build;
+    }
+
+    public RetryPolicy buildCasRetryPolicy() {
+      return new BoundedExponentialBackoffRetry(
+          casBaseSleepTimeMs, casMaxSleepTimeMs, casMaxRetries);
+    }
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java
index 0cd3f8a..43fab21 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java
@@ -14,12 +14,18 @@
 
 package com.googlesource.gerrit.plugins.multisite;
 
-import com.google.gerrit.extensions.annotations.PluginData;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.ModuleImpl;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gson.Gson;
+import com.google.inject.CreationException;
 import com.google.inject.Inject;
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
+import com.google.inject.spi.Message;
+import com.googlesource.gerrit.plugins.multisite.broker.BrokerGson;
 import com.googlesource.gerrit.plugins.multisite.broker.GsonProvider;
 import com.googlesource.gerrit.plugins.multisite.cache.CacheModule;
 import com.googlesource.gerrit.plugins.multisite.event.EventModule;
@@ -34,22 +40,42 @@
 import java.io.FileReader;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Collection;
 import java.util.UUID;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+@ModuleImpl(name = GitRepositoryManager.LOCAL_REPOSITORY_MODULE)
 public class Module extends LifecycleModule {
   private static final Logger log = LoggerFactory.getLogger(Module.class);
   private final Configuration config;
+  private final boolean disableGitRepositoryValidation;
 
   @Inject
   public Module(Configuration config) {
+    this(config, false);
+  }
+
+  // TODO: It is not possible to properly test the libModules in Gerrit.
+  // Disable the Git repository validation during integration test and then build the necessary
+  // support
+  // in Gerrit for it.
+  @VisibleForTesting
+  public Module(Configuration config, boolean disableGitRepositoryValidation) {
     this.config = config;
+    this.disableGitRepositoryValidation = disableGitRepositoryValidation;
   }
 
   @Override
   protected void configure() {
+
+    Collection<Message> validationErrors = config.validate();
+    if (!validationErrors.isEmpty()) {
+      throw new CreationException(validationErrors);
+    }
+
     listener().to(Log4jMessageLogger.class);
     bind(MessageLogger.class).to(Log4jMessageLogger.class);
 
@@ -73,16 +99,22 @@
       install(new BrokerForwarderModule(config.kafkaPublisher()));
     }
 
-    install(new ValidationModule());
-
-    bind(Gson.class).toProvider(GsonProvider.class).in(Singleton.class);
+    install(new ValidationModule(config, disableGitRepositoryValidation));
+    bind(Gson.class)
+        .annotatedWith(BrokerGson.class)
+        .toProvider(GsonProvider.class)
+        .in(Singleton.class);
   }
 
   @Provides
   @Singleton
   @InstanceId
-  UUID getInstanceId(@PluginData java.nio.file.Path dataDir) throws IOException {
+  UUID getInstanceId(SitePaths sitePaths) throws IOException {
     UUID instanceId = null;
+    Path dataDir = sitePaths.data_dir.resolve(Configuration.PLUGIN_NAME);
+    if (!dataDir.toFile().exists()) {
+      dataDir.toFile().mkdirs();
+    }
     String serverIdFile =
         dataDir.toAbsolutePath().toString() + "/" + Configuration.INSTANCE_ID_FILE;
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/broker/BrokerGson.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/broker/BrokerGson.java
new file mode 100644
index 0000000..219aa96
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/broker/BrokerGson.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.multisite.broker;
+
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(RUNTIME)
+@Target(PARAMETER)
+@BindingAnnotation
+public @interface BrokerGson {}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/broker/BrokerPublisher.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/broker/BrokerPublisher.java
index cce9cc5..4dca9e9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/broker/BrokerPublisher.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/broker/BrokerPublisher.java
@@ -24,6 +24,7 @@
 import com.googlesource.gerrit.plugins.multisite.InstanceId;
 import com.googlesource.gerrit.plugins.multisite.MessageLogger;
 import com.googlesource.gerrit.plugins.multisite.MessageLogger.Direction;
+import com.googlesource.gerrit.plugins.multisite.forwarder.Context;
 import com.googlesource.gerrit.plugins.multisite.forwarder.events.EventFamily;
 import com.googlesource.gerrit.plugins.multisite.kafka.consumer.SourceAwareEventWrapper;
 import java.util.UUID;
@@ -41,7 +42,10 @@
 
   @Inject
   public BrokerPublisher(
-      BrokerSession session, Gson gson, @InstanceId UUID instanceId, MessageLogger msgLog) {
+      BrokerSession session,
+      @BrokerGson Gson gson,
+      @InstanceId UUID instanceId,
+      MessageLogger msgLog) {
     this.session = session;
     this.gson = gson;
     this.instanceId = instanceId;
@@ -63,6 +67,10 @@
   }
 
   public boolean publishEvent(EventFamily eventType, Event event) {
+    if (Context.isForwardedEvent()) {
+      return true;
+    }
+
     SourceAwareEventWrapper brokerEvent = toBrokerEvent(event);
     msgLog.log(Direction.PUBLISH, brokerEvent);
     return session.publishEvent(eventType, getPayload(brokerEvent));
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/cache/CacheEvictionHandler.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/cache/CacheEvictionHandler.java
index 31c79a2..77c19c1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/cache/CacheEvictionHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/cache/CacheEvictionHandler.java
@@ -15,7 +15,6 @@
 package com.googlesource.gerrit.plugins.multisite.cache;
 
 import com.google.common.cache.RemovalNotification;
-import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.cache.CacheRemovalListener;
 import com.google.inject.Inject;
@@ -27,18 +26,15 @@
 class CacheEvictionHandler<K, V> implements CacheRemovalListener<K, V> {
   private final Executor executor;
   private final DynamicSet<CacheEvictionForwarder> forwarders;
-  private final String pluginName;
   private final CachePatternMatcher matcher;
 
   @Inject
   CacheEvictionHandler(
       DynamicSet<CacheEvictionForwarder> forwarders,
       @CacheExecutor Executor executor,
-      @PluginName String pluginName,
       CachePatternMatcher matcher) {
     this.forwarders = forwarders;
     this.executor = executor;
-    this.pluginName = pluginName;
     this.matcher = matcher;
   }
 
@@ -64,8 +60,8 @@
     @Override
     public String toString() {
       return String.format(
-          "[%s] Evict key '%s' from cache '%s' in target instance",
-          pluginName, cacheEvictionEvent.key, cacheEvictionEvent.cacheName);
+          "Evict key '%s' from cache '%s' in target instance",
+          cacheEvictionEvent.key, cacheEvictionEvent.cacheName);
     }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/cache/ProjectListUpdateHandler.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/cache/ProjectListUpdateHandler.java
index 36de1bb..5723fbe 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/cache/ProjectListUpdateHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/cache/ProjectListUpdateHandler.java
@@ -14,7 +14,6 @@
 
 package com.googlesource.gerrit.plugins.multisite.cache;
 
-import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.events.NewProjectCreatedListener;
 import com.google.gerrit.extensions.events.ProjectDeletedListener;
 import com.google.gerrit.extensions.events.ProjectEvent;
@@ -31,16 +30,12 @@
 
   private final DynamicSet<ProjectListUpdateForwarder> forwarders;
   private final Executor executor;
-  private final String pluginName;
 
   @Inject
   public ProjectListUpdateHandler(
-      DynamicSet<ProjectListUpdateForwarder> forwarders,
-      @CacheExecutor Executor executor,
-      @PluginName String pluginName) {
+      DynamicSet<ProjectListUpdateForwarder> forwarders, @CacheExecutor Executor executor) {
     this.forwarders = forwarders;
     this.executor = executor;
-    this.pluginName = pluginName;
   }
 
   @Override
@@ -77,10 +72,8 @@
     @Override
     public String toString() {
       return String.format(
-          "[%s] Update project list in target instance: %s '%s'",
-          pluginName,
-          projectListUpdateEvent.remove ? "remove" : "add",
-          projectListUpdateEvent.projectName);
+          "Update project list in target instance: %s '%s'",
+          projectListUpdateEvent.remove ? "remove" : "add", projectListUpdateEvent.projectName);
     }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/event/EventHandler.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/event/EventHandler.java
index 977cdae..aafd59f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/event/EventHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/event/EventHandler.java
@@ -14,7 +14,6 @@
 
 package com.googlesource.gerrit.plugins.multisite.event;
 
-import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.EventListener;
@@ -27,16 +26,11 @@
 class EventHandler implements EventListener {
   private final Executor executor;
   private final DynamicSet<StreamEventForwarder> forwarders;
-  private final String pluginName;
 
   @Inject
-  EventHandler(
-      DynamicSet<StreamEventForwarder> forwarders,
-      @EventExecutor Executor executor,
-      @PluginName String pluginName) {
+  EventHandler(DynamicSet<StreamEventForwarder> forwarders, @EventExecutor Executor executor) {
     this.forwarders = forwarders;
     this.executor = executor;
-    this.pluginName = pluginName;
   }
 
   @Override
@@ -60,7 +54,7 @@
 
     @Override
     public String toString() {
-      return String.format("[%s] Send event '%s' to target instance", pluginName, event.type);
+      return String.format("Send event '%s' to target instance", event.type);
     }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedAwareEventBroker.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedAwareEventBroker.java
index bbe7301..e69de29 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedAwareEventBroker.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedAwareEventBroker.java
@@ -1,45 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed 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 com.googlesource.gerrit.plugins.multisite.forwarder;
-
-import com.google.gerrit.server.events.Event;
-import com.google.gerrit.server.events.EventBroker;
-import com.google.gerrit.server.events.EventListener;
-import com.google.gerrit.server.events.UserScopedEventListener;
-import com.google.gerrit.server.notedb.ChangeNotes.Factory;
-import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.inject.Inject;
-
-class ForwardedAwareEventBroker extends EventBroker {
-
-  @Inject
-  ForwardedAwareEventBroker(
-      PluginSetContext<UserScopedEventListener> listeners,
-      PluginSetContext<EventListener> unrestrictedListeners,
-      PermissionBackend permissionBackend,
-      ProjectCache projectCache,
-      Factory notesFactory) {
-    super(listeners, unrestrictedListeners, permissionBackend, projectCache, notesFactory);
-  }
-
-  @Override
-  protected void fireEventForUnrestrictedListeners(Event event) {
-    if (!Context.isForwardedEvent()) {
-      super.fireEventForUnrestrictedListeners(event);
-    }
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwarderModule.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwarderModule.java
index 6df5f51..ca17004 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwarderModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwarderModule.java
@@ -14,16 +14,13 @@
 
 package com.googlesource.gerrit.plugins.multisite.forwarder;
 
-import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.server.events.EventDispatcher;
 import com.google.inject.AbstractModule;
 
 public class ForwarderModule extends AbstractModule {
 
   @Override
   protected void configure() {
-    DynamicItem.bind(binder(), EventDispatcher.class).to(ForwardedAwareEventBroker.class);
     DynamicSet.setOf(binder(), CacheEvictionForwarder.class);
     DynamicSet.setOf(binder(), IndexEventForwarder.class);
     DynamicSet.setOf(binder(), ProjectListUpdateForwarder.class);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/broker/BrokerForwarderModule.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/broker/BrokerForwarderModule.java
index 8a3b6dc..7c6bef3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/broker/BrokerForwarderModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/broker/BrokerForwarderModule.java
@@ -16,12 +16,9 @@
 
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gson.Gson;
-import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.multisite.Configuration.KafkaPublisher;
 import com.googlesource.gerrit.plugins.multisite.broker.BrokerPublisher;
 import com.googlesource.gerrit.plugins.multisite.broker.BrokerSession;
-import com.googlesource.gerrit.plugins.multisite.broker.GsonProvider;
 import com.googlesource.gerrit.plugins.multisite.broker.kafka.KafkaSession;
 import com.googlesource.gerrit.plugins.multisite.forwarder.CacheEvictionForwarder;
 import com.googlesource.gerrit.plugins.multisite.forwarder.IndexEventForwarder;
@@ -38,7 +35,6 @@
 
   @Override
   protected void configure() {
-    bind(Gson.class).toProvider(GsonProvider.class).in(Singleton.class);
     listener().to(BrokerPublisher.class);
     bind(BrokerSession.class).to(KafkaSession.class);
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandler.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandler.java
index 9bef6fb..ee3ddbc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandler.java
@@ -15,7 +15,6 @@
 package com.googlesource.gerrit.plugins.multisite.index;
 
 import com.google.common.base.Objects;
-import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.events.AccountIndexedListener;
 import com.google.gerrit.extensions.events.ChangeIndexedListener;
 import com.google.gerrit.extensions.events.GroupIndexedListener;
@@ -43,19 +42,16 @@
   private static final Logger log = LoggerFactory.getLogger(IndexEventHandler.class);
   private final Executor executor;
   private final DynamicSet<IndexEventForwarder> forwarders;
-  private final String pluginName;
   private final Set<IndexTask> queuedTasks = Collections.newSetFromMap(new ConcurrentHashMap<>());
   private final ChangeCheckerImpl.Factory changeChecker;
 
   @Inject
   IndexEventHandler(
       @IndexExecutor Executor executor,
-      @PluginName String pluginName,
       DynamicSet<IndexEventForwarder> forwarders,
       ChangeCheckerImpl.Factory changeChecker) {
     this.forwarders = forwarders;
     this.executor = executor;
-    this.pluginName = pluginName;
     this.changeChecker = changeChecker;
   }
 
@@ -164,8 +160,7 @@
 
     @Override
     public String toString() {
-      return String.format(
-          "[%s] Index change %s in target instance", pluginName, changeIndexEvent.changeId);
+      return String.format("Index change %s in target instance", changeIndexEvent.changeId);
     }
   }
 
@@ -196,8 +191,7 @@
 
     @Override
     public String toString() {
-      return String.format(
-          "[%s] Index account %s in target instance", pluginName, accountIndexEvent.accountId);
+      return String.format("Index account %s in target instance", accountIndexEvent.accountId);
     }
   }
 
@@ -228,8 +222,7 @@
 
     @Override
     public String toString() {
-      return String.format(
-          "[%s] Index group %s in target instance", pluginName, groupIndexEvent.groupUUID);
+      return String.format("Index group %s in target instance", groupIndexEvent.groupUUID);
     }
   }
 
@@ -260,8 +253,7 @@
 
     @Override
     public String toString() {
-      return String.format(
-          "[%s] Index project %s in target instance", pluginName, projectIndexEvent.projectName);
+      return String.format("Index project %s in target instance", projectIndexEvent.projectName);
     }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/AbstractKafkaSubcriber.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/AbstractKafkaSubcriber.java
index fa73964..25200ad 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/AbstractKafkaSubcriber.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/AbstractKafkaSubcriber.java
@@ -21,11 +21,11 @@
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gson.Gson;
 import com.google.gwtorm.server.OrmException;
-import com.google.inject.Provider;
 import com.googlesource.gerrit.plugins.multisite.Configuration;
 import com.googlesource.gerrit.plugins.multisite.InstanceId;
 import com.googlesource.gerrit.plugins.multisite.MessageLogger;
 import com.googlesource.gerrit.plugins.multisite.MessageLogger.Direction;
+import com.googlesource.gerrit.plugins.multisite.broker.BrokerGson;
 import com.googlesource.gerrit.plugins.multisite.forwarder.events.EventFamily;
 import com.googlesource.gerrit.plugins.multisite.kafka.router.ForwardedEventRouter;
 import java.io.IOException;
@@ -46,7 +46,7 @@
   private final KafkaConsumer<byte[], byte[]> consumer;
   private final ForwardedEventRouter eventRouter;
   private final DynamicSet<DroppedEventListener> droppedEventListeners;
-  private final Provider<Gson> gsonProvider;
+  private final Gson gson;
   private final UUID instanceId;
   private final AtomicBoolean closed = new AtomicBoolean(false);
   private final Deserializer<SourceAwareEventWrapper> valueDeserializer;
@@ -60,14 +60,14 @@
       Deserializer<SourceAwareEventWrapper> valueDeserializer,
       ForwardedEventRouter eventRouter,
       DynamicSet<DroppedEventListener> droppedEventListeners,
-      Provider<Gson> gsonProvider,
+      @BrokerGson Gson gson,
       @InstanceId UUID instanceId,
       OneOffRequestContext oneOffCtx,
       MessageLogger msgLog) {
     this.configuration = configuration;
     this.eventRouter = eventRouter;
     this.droppedEventListeners = droppedEventListeners;
-    this.gsonProvider = gsonProvider;
+    this.gson = gson;
     this.instanceId = instanceId;
     this.oneOffCtx = oneOffCtx;
     this.msgLog = msgLog;
@@ -122,7 +122,7 @@
       } else {
         try {
           msgLog.log(Direction.CONSUME, event);
-          eventRouter.route(event.getEventBody(gsonProvider));
+          eventRouter.route(event.getEventBody(gson));
         } catch (IOException e) {
           logger.atSevere().withCause(e).log(
               "Malformed event '%s': [Exception: %s]", event.getHeader().getEventType());
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/CacheEvictionEventSubscriber.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/CacheEvictionEventSubscriber.java
index 0e33c00..8e724be 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/CacheEvictionEventSubscriber.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/CacheEvictionEventSubscriber.java
@@ -18,11 +18,11 @@
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gson.Gson;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.multisite.Configuration;
 import com.googlesource.gerrit.plugins.multisite.InstanceId;
 import com.googlesource.gerrit.plugins.multisite.MessageLogger;
+import com.googlesource.gerrit.plugins.multisite.broker.BrokerGson;
 import com.googlesource.gerrit.plugins.multisite.forwarder.events.EventFamily;
 import com.googlesource.gerrit.plugins.multisite.kafka.router.StreamEventRouter;
 import java.util.UUID;
@@ -37,7 +37,7 @@
       Deserializer<SourceAwareEventWrapper> valueDeserializer,
       StreamEventRouter eventRouter,
       DynamicSet<DroppedEventListener> droppedEventListeners,
-      Provider<Gson> gsonProvider,
+      @BrokerGson Gson gsonProvider,
       @InstanceId UUID instanceId,
       OneOffRequestContext oneOffCtx,
       MessageLogger msgLog) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/IndexEventSubscriber.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/IndexEventSubscriber.java
index b61d23d..e52b624 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/IndexEventSubscriber.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/IndexEventSubscriber.java
@@ -18,11 +18,11 @@
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gson.Gson;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.multisite.Configuration;
 import com.googlesource.gerrit.plugins.multisite.InstanceId;
 import com.googlesource.gerrit.plugins.multisite.MessageLogger;
+import com.googlesource.gerrit.plugins.multisite.broker.BrokerGson;
 import com.googlesource.gerrit.plugins.multisite.forwarder.events.EventFamily;
 import com.googlesource.gerrit.plugins.multisite.kafka.router.IndexEventRouter;
 import java.util.UUID;
@@ -37,7 +37,7 @@
       Deserializer<SourceAwareEventWrapper> valueDeserializer,
       IndexEventRouter eventRouter,
       DynamicSet<DroppedEventListener> droppedEventListeners,
-      Provider<Gson> gsonProvider,
+      @BrokerGson Gson gsonProvider,
       @InstanceId UUID instanceId,
       OneOffRequestContext oneOffCtx,
       MessageLogger msgLog) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/KafkaEventDeserializer.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/KafkaEventDeserializer.java
index 9a589df..6e86d25 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/KafkaEventDeserializer.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/KafkaEventDeserializer.java
@@ -16,8 +16,8 @@
 
 import com.google.gson.Gson;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.multisite.broker.BrokerGson;
 import java.util.Map;
 import org.apache.kafka.common.serialization.Deserializer;
 import org.apache.kafka.common.serialization.StringDeserializer;
@@ -26,29 +26,24 @@
 public class KafkaEventDeserializer implements Deserializer<SourceAwareEventWrapper> {
 
   private final StringDeserializer stringDeserializer = new StringDeserializer();
-  private Provider<Gson> gsonProvider;
+  private Gson gson;
 
   // To be used when providing this deserializer with class name (then need to add a configuration
   // entry to set the gson.provider
   public KafkaEventDeserializer() {}
 
   @Inject
-  public KafkaEventDeserializer(Provider<Gson> gsonProvider) {
-    this.gsonProvider = gsonProvider;
+  public KafkaEventDeserializer(@BrokerGson Gson gson) {
+    this.gson = gson;
   }
 
-  @SuppressWarnings("unchecked")
   @Override
-  public void configure(Map<String, ?> configs, boolean isKey) {
-    gsonProvider = (Provider<Gson>) configs.get("gson.provider");
-  }
+  public void configure(Map<String, ?> configs, boolean isKey) {}
 
   @Override
   public SourceAwareEventWrapper deserialize(String topic, byte[] data) {
     final SourceAwareEventWrapper result =
-        gsonProvider
-            .get()
-            .fromJson(stringDeserializer.deserialize(topic, data), SourceAwareEventWrapper.class);
+        gson.fromJson(stringDeserializer.deserialize(topic, data), SourceAwareEventWrapper.class);
 
     result.validate();
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/ProjectUpdateEventSubscriber.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/ProjectUpdateEventSubscriber.java
index 207848e..a949cd2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/ProjectUpdateEventSubscriber.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/ProjectUpdateEventSubscriber.java
@@ -18,11 +18,11 @@
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gson.Gson;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.multisite.Configuration;
 import com.googlesource.gerrit.plugins.multisite.InstanceId;
 import com.googlesource.gerrit.plugins.multisite.MessageLogger;
+import com.googlesource.gerrit.plugins.multisite.broker.BrokerGson;
 import com.googlesource.gerrit.plugins.multisite.forwarder.events.EventFamily;
 import com.googlesource.gerrit.plugins.multisite.kafka.router.ProjectListUpdateRouter;
 import java.util.UUID;
@@ -37,7 +37,7 @@
       Deserializer<SourceAwareEventWrapper> valueDeserializer,
       ProjectListUpdateRouter eventRouter,
       DynamicSet<DroppedEventListener> droppedEventListeners,
-      Provider<Gson> gsonProvider,
+      @BrokerGson Gson gson,
       @InstanceId UUID instanceId,
       OneOffRequestContext oneOffCtx,
       MessageLogger msgLog) {
@@ -47,7 +47,7 @@
         valueDeserializer,
         eventRouter,
         droppedEventListeners,
-        gsonProvider,
+        gson,
         instanceId,
         oneOffCtx,
         msgLog);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/SourceAwareEventWrapper.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/SourceAwareEventWrapper.java
index a618dfc..09fbe0d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/SourceAwareEventWrapper.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/SourceAwareEventWrapper.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.server.events.Event;
 import com.google.gson.Gson;
 import com.google.gson.JsonObject;
-import com.google.inject.Provider;
 import java.util.UUID;
 import org.apache.commons.lang3.Validate;
 
@@ -34,8 +33,8 @@
     return body;
   }
 
-  public Event getEventBody(Provider<Gson> gsonProvider) {
-    return gsonProvider.get().fromJson(this.body, Event.class);
+  public Event getEventBody(Gson gson) {
+    return gson.fromJson(this.body, Event.class);
   }
 
   public static class EventHeader {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/StreamEventSubscriber.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/StreamEventSubscriber.java
index c55be53..d43a3c3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/StreamEventSubscriber.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/StreamEventSubscriber.java
@@ -18,11 +18,11 @@
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gson.Gson;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.multisite.Configuration;
 import com.googlesource.gerrit.plugins.multisite.InstanceId;
 import com.googlesource.gerrit.plugins.multisite.MessageLogger;
+import com.googlesource.gerrit.plugins.multisite.broker.BrokerGson;
 import com.googlesource.gerrit.plugins.multisite.forwarder.events.EventFamily;
 import com.googlesource.gerrit.plugins.multisite.kafka.router.StreamEventRouter;
 import java.util.UUID;
@@ -37,7 +37,7 @@
       Deserializer<SourceAwareEventWrapper> valueDeserializer,
       StreamEventRouter eventRouter,
       DynamicSet<DroppedEventListener> droppedEventListeners,
-      Provider<Gson> gsonProvider,
+      @BrokerGson Gson gson,
       @InstanceId UUID instanceId,
       OneOffRequestContext oneOffCtx,
       MessageLogger msgLog) {
@@ -47,7 +47,7 @@
         valueDeserializer,
         eventRouter,
         droppedEventListeners,
-        gsonProvider,
+        gson,
         instanceId,
         oneOffCtx,
         msgLog);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/InSyncChangeValidator.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/InSyncChangeValidator.java
deleted file mode 100644
index 09a1b56..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/InSyncChangeValidator.java
+++ /dev/null
@@ -1,198 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed 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.
-// Copyright (C) 2018 The Android Open Source Project
-//
-// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation;
-
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.server.events.RefReceivedEvent;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.validators.RefOperationValidationListener;
-import com.google.gerrit.server.git.validators.ValidationMessage;
-import com.google.gerrit.server.validators.ValidationException;
-import com.google.inject.Inject;
-import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefDatabase;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.List;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-
-/**
- * Validates if a change can be applied without bringing the system into a split brain situation by
- * verifying that the local status is aligned with the central status as retrieved by the
- * SharedRefDatabase. It also updates the DB to set the new current status for a ref as a
- * consequence of ref updates, creation and deletions. The operation is done for mutable updates
- * only. Operation on immutable ones are always considered valid.
- */
-public class InSyncChangeValidator implements RefOperationValidationListener {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  private final SharedRefDatabase dfsRefDatabase;
-  private final GitRepositoryManager repoManager;
-
-  @Inject
-  public InSyncChangeValidator(SharedRefDatabase dfsRefDatabase, GitRepositoryManager repoManager) {
-    this.dfsRefDatabase = dfsRefDatabase;
-    this.repoManager = repoManager;
-  }
-
-  @Override
-  public List<ValidationMessage> onRefOperation(RefReceivedEvent refEvent)
-      throws ValidationException {
-    logger.atFine().log("Validating operation %s", refEvent);
-
-    if (isImmutableRef(refEvent.getRefName())) {
-      return Collections.emptyList();
-    }
-
-    try (Repository repo = repoManager.openRepository(refEvent.getProjectNameKey())) {
-
-      switch (refEvent.command.getType()) {
-        case CREATE:
-          return onCreateRef(refEvent);
-
-        case UPDATE:
-        case UPDATE_NONFASTFORWARD:
-          return onUpdateRef(repo, refEvent);
-
-        case DELETE:
-          return onDeleteRef(repo, refEvent);
-
-        default:
-          throw new IllegalArgumentException(
-              String.format(
-                  "Unsupported command type '%s', in event %s",
-                  refEvent.command.getType().name(), refEvent));
-      }
-    } catch (IOException e) {
-      throw new ValidationException(
-          "Unable to access repository " + refEvent.getProjectNameKey(), e);
-    }
-  }
-
-  private boolean isImmutableRef(String refName) {
-    return refName.startsWith("refs/changes") && !refName.endsWith("/meta");
-  }
-
-  private List<ValidationMessage> onDeleteRef(Repository repo, RefReceivedEvent refEvent)
-      throws ValidationException {
-    try {
-      Ref localRef = repo.findRef(refEvent.getRefName());
-      if (localRef == null) {
-        logger.atWarning().log(
-            "Local status inconsistent with shared ref database for ref %s. "
-                + "Trying to delete it but it is not in the local DB",
-            refEvent.getRefName());
-
-        throw new ValidationException(
-            String.format(
-                "Unable to delete ref '%s', cannot find it in the local ref database",
-                refEvent.getRefName()));
-      }
-
-      if (!dfsRefDatabase.compareAndRemove(refEvent.getProjectNameKey().get(), localRef)) {
-        throw new ValidationException(
-            String.format(
-                "Unable to delete ref '%s', the local ObjectId '%s' is not equal to the one "
-                    + "in the shared ref database",
-                refEvent.getRefName(), localRef.getObjectId().getName()));
-      }
-    } catch (IOException ioe) {
-      logger.atSevere().withCause(ioe).log(
-          "Local status inconsistent with shared ref database for ref %s. "
-              + "Trying to delete it but it is not in the DB",
-          refEvent.getRefName());
-
-      throw new ValidationException(
-          String.format(
-              "Unable to delete ref '%s', cannot find it in the shared ref database",
-              refEvent.getRefName()),
-          ioe);
-    }
-    return Collections.emptyList();
-  }
-
-  private List<ValidationMessage> onUpdateRef(Repository repo, RefReceivedEvent refEvent)
-      throws ValidationException {
-    try {
-      Ref localRef = repo.findRef(refEvent.getRefName());
-      if (localRef == null) {
-        logger.atWarning().log(
-            "Local status inconsistent with shared ref database for ref %s. "
-                + "Trying to update it but it is not in the local DB",
-            refEvent.getRefName());
-
-        throw new ValidationException(
-            String.format(
-                "Unable to update ref '%s', cannot find it in the local ref database",
-                refEvent.getRefName()));
-      }
-
-      Ref newRef = dfsRefDatabase.newRef(refEvent.getRefName(), refEvent.command.getNewId());
-      if (!dfsRefDatabase.compareAndPut(refEvent.getProjectNameKey().get(), localRef, newRef)) {
-        throw new ValidationException(
-            String.format(
-                "Unable to update ref '%s', the local objectId '%s' is not equal to the one "
-                    + "in the shared ref database",
-                refEvent.getRefName(), localRef.getObjectId().getName()));
-      }
-    } catch (IOException ioe) {
-      logger.atSevere().withCause(ioe).log(
-          "Local status inconsistent with shared ref database for ref %s. "
-              + "Trying to update it cannot extract the existing one on DB",
-          refEvent.getRefName());
-
-      throw new ValidationException(
-          String.format(
-              "Unable to update ref '%s', cannot open the local ref on the local DB",
-              refEvent.getRefName()),
-          ioe);
-    }
-
-    return Collections.emptyList();
-  }
-
-  private List<ValidationMessage> onCreateRef(RefReceivedEvent refEvent)
-      throws ValidationException {
-    try {
-      Ref newRef = dfsRefDatabase.newRef(refEvent.getRefName(), refEvent.command.getNewId());
-      dfsRefDatabase.compareAndCreate(refEvent.getProjectNameKey().get(), newRef);
-    } catch (IllegalArgumentException | IOException alreadyInDB) {
-      logger.atSevere().withCause(alreadyInDB).log(
-          "Local status inconsistent with shared ref database for ref %s. "
-              + "Trying to delete it but it is not in the DB",
-          refEvent.getRefName());
-
-      throw new ValidationException(
-          String.format(
-              "Unable to update ref '%s', cannot find it in the shared ref database",
-              refEvent.getRefName()),
-          alreadyInDB);
-    }
-    return Collections.emptyList();
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteBatchRefUpdate.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteBatchRefUpdate.java
new file mode 100644
index 0000000..783f6a2
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteBatchRefUpdate.java
@@ -0,0 +1,276 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed 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.
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation;
+
+import static java.util.Comparator.comparing;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefDatabase;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.util.time.ProposedTimestamp;
+
+public class MultiSiteBatchRefUpdate extends BatchRefUpdate {
+  private final BatchRefUpdate batchRefUpdate;
+  private final RefDatabase refDb;
+  private final SharedRefDatabase sharedRefDb;
+  private final String projectName;
+
+  public static class RefPair {
+    final Ref oldRef;
+    final Ref newRef;
+    final Exception exception;
+
+    RefPair(Ref oldRef, Ref newRef) {
+      this.oldRef = oldRef;
+      this.newRef = newRef;
+      this.exception = null;
+    }
+
+    RefPair(Ref newRef, Exception e) {
+      this.newRef = newRef;
+      this.oldRef = SharedRefDatabase.NULL_REF;
+      this.exception = e;
+    }
+
+    public boolean hasFailed() {
+      return exception != null;
+    }
+  }
+
+  public static interface Factory {
+    MultiSiteBatchRefUpdate create(String projectName, RefDatabase refDb);
+  }
+
+  @Inject
+  public MultiSiteBatchRefUpdate(
+      SharedRefDatabase sharedRefDb, @Assisted String projectName, @Assisted RefDatabase refDb) {
+    super(refDb);
+
+    this.sharedRefDb = sharedRefDb;
+    this.projectName = projectName;
+    this.refDb = refDb;
+    this.batchRefUpdate = refDb.newBatchUpdate();
+  }
+
+  @Override
+  public int hashCode() {
+    return batchRefUpdate.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    return batchRefUpdate.equals(obj);
+  }
+
+  @Override
+  public boolean isAllowNonFastForwards() {
+    return batchRefUpdate.isAllowNonFastForwards();
+  }
+
+  @Override
+  public BatchRefUpdate setAllowNonFastForwards(boolean allow) {
+    return batchRefUpdate.setAllowNonFastForwards(allow);
+  }
+
+  @Override
+  public PersonIdent getRefLogIdent() {
+    return batchRefUpdate.getRefLogIdent();
+  }
+
+  @Override
+  public BatchRefUpdate setRefLogIdent(PersonIdent pi) {
+    return batchRefUpdate.setRefLogIdent(pi);
+  }
+
+  @Override
+  public String getRefLogMessage() {
+    return batchRefUpdate.getRefLogMessage();
+  }
+
+  @Override
+  public boolean isRefLogIncludingResult() {
+    return batchRefUpdate.isRefLogIncludingResult();
+  }
+
+  @Override
+  public BatchRefUpdate setRefLogMessage(String msg, boolean appendStatus) {
+    return batchRefUpdate.setRefLogMessage(msg, appendStatus);
+  }
+
+  @Override
+  public BatchRefUpdate disableRefLog() {
+    return batchRefUpdate.disableRefLog();
+  }
+
+  @Override
+  public BatchRefUpdate setForceRefLog(boolean force) {
+    return batchRefUpdate.setForceRefLog(force);
+  }
+
+  @Override
+  public boolean isRefLogDisabled() {
+    return batchRefUpdate.isRefLogDisabled();
+  }
+
+  @Override
+  public BatchRefUpdate setAtomic(boolean atomic) {
+    return batchRefUpdate.setAtomic(atomic);
+  }
+
+  @Override
+  public boolean isAtomic() {
+    return batchRefUpdate.isAtomic();
+  }
+
+  @Override
+  public void setPushCertificate(PushCertificate cert) {
+    batchRefUpdate.setPushCertificate(cert);
+  }
+
+  @Override
+  public List<ReceiveCommand> getCommands() {
+    return batchRefUpdate.getCommands();
+  }
+
+  @Override
+  public BatchRefUpdate addCommand(ReceiveCommand cmd) {
+    return batchRefUpdate.addCommand(cmd);
+  }
+
+  @Override
+  public BatchRefUpdate addCommand(ReceiveCommand... cmd) {
+    return batchRefUpdate.addCommand(cmd);
+  }
+
+  @Override
+  public BatchRefUpdate addCommand(Collection<ReceiveCommand> cmd) {
+    return batchRefUpdate.addCommand(cmd);
+  }
+
+  @Override
+  public List<String> getPushOptions() {
+    return batchRefUpdate.getPushOptions();
+  }
+
+  @Override
+  public List<ProposedTimestamp> getProposedTimestamps() {
+    return batchRefUpdate.getProposedTimestamps();
+  }
+
+  @Override
+  public BatchRefUpdate addProposedTimestamp(ProposedTimestamp ts) {
+    return batchRefUpdate.addProposedTimestamp(ts);
+  }
+
+  @Override
+  public void execute(RevWalk walk, ProgressMonitor monitor, List<String> options)
+      throws IOException {
+    updateSharedRefDb(getRefsPairs());
+    batchRefUpdate.execute(walk, monitor, options);
+  }
+
+  @Override
+  public void execute(RevWalk walk, ProgressMonitor monitor) throws IOException {
+    updateSharedRefDb(getRefsPairs());
+    batchRefUpdate.execute(walk, monitor);
+  }
+
+  @Override
+  public String toString() {
+    return batchRefUpdate.toString();
+  }
+
+  private void updateSharedRefDb(Stream<RefPair> oldRefs) throws IOException {
+    List<RefPair> refsToUpdate =
+        oldRefs.sorted(comparing(RefPair::hasFailed).reversed()).collect(Collectors.toList());
+    if (refsToUpdate.isEmpty()) {
+      return;
+    }
+
+    if (refsToUpdate.get(0).hasFailed()) {
+      RefPair failedRef = refsToUpdate.get(0);
+      throw new IOException(
+          "Failed to fetch ref entries" + failedRef.newRef.getName(), failedRef.exception);
+    }
+
+    for (RefPair refPair : refsToUpdate) {
+      boolean compareAndPutResult =
+          sharedRefDb.compareAndPut(projectName, refPair.oldRef, refPair.newRef);
+      if (!compareAndPutResult) {
+        throw new IOException(
+            String.format(
+                "This repos is out of sync for project %s. old_ref=%s, new_ref=%s",
+                projectName, refPair.oldRef, refPair.newRef));
+      }
+    }
+  }
+
+  private Stream<RefPair> getRefsPairs() {
+    return batchRefUpdate.getCommands().stream().map(this::getRefPairForCommand);
+  }
+
+  private RefPair getRefPairForCommand(ReceiveCommand command) {
+    try {
+      switch (command.getType()) {
+        case CREATE:
+          return new RefPair(SharedRefDatabase.NULL_REF, getNewRef(command));
+
+        case UPDATE:
+        case UPDATE_NONFASTFORWARD:
+          return new RefPair(refDb.getRef(command.getRefName()), getNewRef(command));
+
+        case DELETE:
+          return new RefPair(refDb.getRef(command.getRefName()), SharedRefDatabase.NULL_REF);
+
+        default:
+          return new RefPair(
+              getNewRef(command),
+              new IllegalArgumentException("Unsupported command type " + command.getType()));
+      }
+    } catch (IOException e) {
+      return new RefPair(command.getRef(), e);
+    }
+  }
+
+  private Ref getNewRef(ReceiveCommand command) {
+    return sharedRefDb.newRef(command.getRefName(), command.getNewId());
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteGitRepositoryManager.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteGitRepositoryManager.java
new file mode 100644
index 0000000..eb7bc81
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteGitRepositoryManager.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed 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.
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation;
+
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gerrit.server.git.RepositoryCaseMismatchException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.SortedSet;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+
+@Singleton
+public class MultiSiteGitRepositoryManager implements GitRepositoryManager {
+  private final GitRepositoryManager gitRepositoryManager;
+  private final MultiSiteRepository.Factory multiSiteRepoFactory;
+
+  @Inject
+  public MultiSiteGitRepositoryManager(
+      MultiSiteRepository.Factory multiSiteRepoFactory,
+      LocalDiskRepositoryManager localDiskRepositoryManager) {
+    this.multiSiteRepoFactory = multiSiteRepoFactory;
+    this.gitRepositoryManager = localDiskRepositoryManager;
+  }
+
+  @Override
+  public Repository openRepository(NameKey name) throws RepositoryNotFoundException, IOException {
+    return wrap(name, gitRepositoryManager.openRepository(name));
+  }
+
+  @Override
+  public Repository createRepository(NameKey name)
+      throws RepositoryCaseMismatchException, RepositoryNotFoundException, IOException {
+    return wrap(name, gitRepositoryManager.createRepository(name));
+  }
+
+  @Override
+  public SortedSet<NameKey> list() {
+    return gitRepositoryManager.list();
+  }
+
+  private Repository wrap(NameKey projectName, Repository projectRepo) {
+    return multiSiteRepoFactory.create(projectName.get(), projectRepo);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRefDatabase.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRefDatabase.java
new file mode 100644
index 0000000..d18bb77
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRefDatabase.java
@@ -0,0 +1,178 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed 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.
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefRename;
+import org.eclipse.jgit.lib.RefUpdate;
+
+public class MultiSiteRefDatabase extends RefDatabase {
+  private final MultiSiteRefUpdate.Factory refUpdateFactory;
+  private final MultiSiteBatchRefUpdate.Factory batchRefUpdateFactory;
+  private final String projectName;
+  private final RefDatabase refDatabase;
+
+  public interface Factory {
+    public MultiSiteRefDatabase create(String projectName, RefDatabase refDatabase);
+  }
+
+  @Inject
+  public MultiSiteRefDatabase(
+      MultiSiteRefUpdate.Factory refUpdateFactory,
+      MultiSiteBatchRefUpdate.Factory batchRefUpdateFactory,
+      @Assisted String projectName,
+      @Assisted RefDatabase refDatabase) {
+    this.refUpdateFactory = refUpdateFactory;
+    this.batchRefUpdateFactory = batchRefUpdateFactory;
+    this.projectName = projectName;
+    this.refDatabase = refDatabase;
+  }
+
+  @Override
+  public int hashCode() {
+    return refDatabase.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    return refDatabase.equals(obj);
+  }
+
+  @Override
+  public void create() throws IOException {
+    refDatabase.create();
+  }
+
+  @Override
+  public void close() {
+    refDatabase.close();
+  }
+
+  @Override
+  public boolean isNameConflicting(String name) throws IOException {
+    return refDatabase.isNameConflicting(name);
+  }
+
+  @Override
+  public Collection<String> getConflictingNames(String name) throws IOException {
+    return refDatabase.getConflictingNames(name);
+  }
+
+  @Override
+  public RefUpdate newUpdate(String name, boolean detach) throws IOException {
+    return wrapRefUpdate(refDatabase.newUpdate(name, detach));
+  }
+
+  @Override
+  public RefRename newRename(String fromName, String toName) throws IOException {
+    return refDatabase.newRename(fromName, toName);
+  }
+
+  @Override
+  public BatchRefUpdate newBatchUpdate() {
+    return batchRefUpdateFactory.create(projectName, refDatabase);
+  }
+
+  @Override
+  public boolean performsAtomicTransactions() {
+    return refDatabase.performsAtomicTransactions();
+  }
+
+  @Override
+  public Ref getRef(String name) throws IOException {
+    return refDatabase.getRef(name);
+  }
+
+  @Override
+  public String toString() {
+    return refDatabase.toString();
+  }
+
+  @Override
+  public Ref exactRef(String name) throws IOException {
+    return refDatabase.exactRef(name);
+  }
+
+  @Override
+  public Map<String, Ref> exactRef(String... refs) throws IOException {
+    return refDatabase.exactRef(refs);
+  }
+
+  @Override
+  public Ref firstExactRef(String... refs) throws IOException {
+    return refDatabase.firstExactRef(refs);
+  }
+
+  @Override
+  public List<Ref> getRefs() throws IOException {
+    return refDatabase.getRefs();
+  }
+
+  @SuppressWarnings("deprecation")
+  @Override
+  public Map<String, Ref> getRefs(String prefix) throws IOException {
+    return refDatabase.getRefs(prefix);
+  }
+
+  @Override
+  public List<Ref> getRefsByPrefix(String prefix) throws IOException {
+    return refDatabase.getRefsByPrefix(prefix);
+  }
+
+  @Override
+  public boolean hasRefs() throws IOException {
+    return refDatabase.hasRefs();
+  }
+
+  @Override
+  public List<Ref> getAdditionalRefs() throws IOException {
+    return refDatabase.getAdditionalRefs();
+  }
+
+  @Override
+  public Ref peel(Ref ref) throws IOException {
+    return refDatabase.peel(ref);
+  }
+
+  @Override
+  public void refresh() {
+    refDatabase.refresh();
+  }
+
+  RefUpdate wrapRefUpdate(RefUpdate refUpdate) {
+    return refUpdateFactory.create(projectName, refUpdate);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRefUpdate.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRefUpdate.java
new file mode 100644
index 0000000..d60082e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRefUpdate.java
@@ -0,0 +1,303 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed 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.
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefDatabase;
+import java.io.IOException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushCertificate;
+
+public class MultiSiteRefUpdate extends RefUpdate {
+
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  protected final RefUpdate refUpdateBase;
+
+  private final SharedRefDatabase sharedDb;
+  private final String projectName;
+
+  public interface Factory {
+    MultiSiteRefUpdate create(String projectName, RefUpdate refUpdate);
+  }
+
+  @Inject
+  public MultiSiteRefUpdate(
+      SharedRefDatabase db, @Assisted String projectName, @Assisted RefUpdate refUpdate) {
+    super(refUpdate.getRef());
+    refUpdateBase = refUpdate;
+    this.sharedDb = db;
+    this.projectName = projectName;
+  }
+
+  private void checkSharedDBForRefUpdate() throws IOException {
+    try {
+      Ref newRef = sharedDb.newRef(refUpdateBase.getName(), refUpdateBase.getNewObjectId());
+
+      if (!sharedDb.compareAndPut(projectName, refUpdateBase.getRef(), newRef)) {
+        throw new IOException(
+            String.format(
+                "Unable to update ref '%s', the local objectId '%s' is not equal to the one "
+                    + "in the shared ref datasuper",
+                newRef.getName(), refUpdateBase.getName()));
+      }
+    } catch (IOException ioe) {
+      logger.atSevere().withCause(ioe).log(
+          "Local status inconsistent with shared ref datasuper for ref %s. "
+              + "Trying to update it cannot extract the existing one on DB",
+          refUpdateBase.getName());
+
+      throw new IOException(
+          String.format(
+              "Unable to update ref '%s', cannot open the local ref on the local DB",
+              refUpdateBase.getName()),
+          ioe);
+    }
+  }
+
+  private void checkSharedDbForRefDelete() throws IOException {
+    Ref oldRef = this.getRef();
+    try {
+      if (!sharedDb.compareAndRemove(projectName, oldRef)) {
+        throw new IOException(
+            String.format(
+                "Unable to delete ref '%s', the local ObjectId '%s' is not equal to the one "
+                    + "in the shared ref database",
+                oldRef.getName(), oldRef.getName()));
+      }
+    } catch (IOException ioe) {
+      logger.atSevere().withCause(ioe).log(
+          "Local status inconsistent with shared ref database for ref %s. "
+              + "Trying to delete it but it is not in the DB",
+          oldRef.getName());
+
+      throw new IOException(
+          String.format(
+              "Unable to delete ref '%s', cannot find it in the shared ref database",
+              oldRef.getName()),
+          ioe);
+    }
+  }
+
+  @Override
+  protected RefDatabase getRefDatabase() {
+    return notImplementedException();
+  }
+
+  private <T> T notImplementedException() {
+    throw new IllegalStateException("This method should have never been invoked");
+  }
+
+  @Override
+  protected Repository getRepository() {
+    return notImplementedException();
+  }
+
+  @Override
+  protected boolean tryLock(boolean deref) throws IOException {
+    return notImplementedException();
+  }
+
+  @Override
+  protected void unlock() {
+    notImplementedException();
+  }
+
+  @Override
+  protected Result doUpdate(Result result) throws IOException {
+    return notImplementedException();
+  }
+
+  @Override
+  protected Result doDelete(Result result) throws IOException {
+    return notImplementedException();
+  }
+
+  @Override
+  protected Result doLink(String target) throws IOException {
+    return notImplementedException();
+  }
+
+  @Override
+  public Result update() throws IOException {
+    checkSharedDBForRefUpdate();
+    return refUpdateBase.update();
+  }
+
+  @Override
+  public Result update(RevWalk rev) throws IOException {
+    checkSharedDBForRefUpdate();
+    return refUpdateBase.update(rev);
+  }
+
+  @Override
+  public Result delete() throws IOException {
+    checkSharedDbForRefDelete();
+    return refUpdateBase.delete();
+  }
+
+  @Override
+  public Result delete(RevWalk walk) throws IOException {
+    checkSharedDbForRefDelete();
+    return refUpdateBase.delete(walk);
+  }
+
+  @Override
+  public int hashCode() {
+    return refUpdateBase.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    return refUpdateBase.equals(obj);
+  }
+
+  @Override
+  public String toString() {
+    return refUpdateBase.toString();
+  }
+
+  @Override
+  public String getName() {
+    return refUpdateBase.getName();
+  }
+
+  @Override
+  public Ref getRef() {
+    return refUpdateBase.getRef();
+  }
+
+  @Override
+  public ObjectId getNewObjectId() {
+    return refUpdateBase.getNewObjectId();
+  }
+
+  @Override
+  public void setDetachingSymbolicRef() {
+    refUpdateBase.setDetachingSymbolicRef();
+  }
+
+  @Override
+  public boolean isDetachingSymbolicRef() {
+    return refUpdateBase.isDetachingSymbolicRef();
+  }
+
+  @Override
+  public void setNewObjectId(AnyObjectId id) {
+    refUpdateBase.setNewObjectId(id);
+  }
+
+  @Override
+  public ObjectId getExpectedOldObjectId() {
+    return refUpdateBase.getExpectedOldObjectId();
+  }
+
+  @Override
+  public void setExpectedOldObjectId(AnyObjectId id) {
+    refUpdateBase.setExpectedOldObjectId(id);
+  }
+
+  @Override
+  public boolean isForceUpdate() {
+    return refUpdateBase.isForceUpdate();
+  }
+
+  @Override
+  public void setForceUpdate(boolean b) {
+    refUpdateBase.setForceUpdate(b);
+  }
+
+  @Override
+  public PersonIdent getRefLogIdent() {
+    return refUpdateBase.getRefLogIdent();
+  }
+
+  @Override
+  public void setRefLogIdent(PersonIdent pi) {
+    refUpdateBase.setRefLogIdent(pi);
+  }
+
+  @Override
+  public String getRefLogMessage() {
+    return refUpdateBase.getRefLogMessage();
+  }
+
+  @Override
+  public void setRefLogMessage(String msg, boolean appendStatus) {
+    refUpdateBase.setRefLogMessage(msg, appendStatus);
+  }
+
+  @Override
+  public void disableRefLog() {
+    refUpdateBase.disableRefLog();
+  }
+
+  @Override
+  public void setForceRefLog(boolean force) {
+    refUpdateBase.setForceRefLog(force);
+  }
+
+  @Override
+  public ObjectId getOldObjectId() {
+    return refUpdateBase.getOldObjectId();
+  }
+
+  @Override
+  public void setPushCertificate(PushCertificate cert) {
+    refUpdateBase.setPushCertificate(cert);
+  }
+
+  @Override
+  public Result getResult() {
+    return refUpdateBase.getResult();
+  }
+
+  @Override
+  public Result forceUpdate() throws IOException {
+    return refUpdateBase.forceUpdate();
+  }
+
+  @Override
+  public Result link(String target) throws IOException {
+    return refUpdateBase.link(target);
+  }
+
+  @Override
+  public void setCheckConflicting(boolean check) {
+    refUpdateBase.setCheckConflicting(check);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRepository.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRepository.java
new file mode 100644
index 0000000..127b35e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRepository.java
@@ -0,0 +1,413 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed 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.
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.eclipse.jgit.attributes.AttributesNodeProvider;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.errors.AmbiguousObjectException;
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.NoWorkTreeException;
+import org.eclipse.jgit.errors.RevisionSyntaxException;
+import org.eclipse.jgit.events.ListenerList;
+import org.eclipse.jgit.events.RepositoryEvent;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.BaseRepositoryBuilder;
+import org.eclipse.jgit.lib.ObjectDatabase;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.RebaseTodoLine;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefRename;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.ReflogReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryState;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.util.FS;
+
+public class MultiSiteRepository extends Repository {
+
+  private final MultiSiteRefDatabase.Factory multiSiteRefDbFactory;
+  private final Repository repository;
+  private final RefDatabase refDatabase;
+  private final MultiSiteRefDatabase multiSiteRefDatabase;
+
+  public interface Factory {
+    public MultiSiteRepository create(String projectName, Repository repository);
+  }
+
+  @Inject
+  public MultiSiteRepository(
+      MultiSiteRefDatabase.Factory multiSiteRefDbFactory,
+      @Assisted String projectName,
+      @Assisted Repository repository) {
+    super(new BaseRepositoryBuilder());
+    this.multiSiteRefDbFactory = multiSiteRefDbFactory;
+    this.repository = repository;
+    this.refDatabase = repository.getRefDatabase();
+    this.multiSiteRefDatabase = multiSiteRefDbFactory.create(projectName, refDatabase);
+  }
+
+  @Override
+  public void create(boolean b) throws IOException {}
+
+  @Override
+  public ObjectDatabase getObjectDatabase() {
+    return repository.getObjectDatabase();
+  }
+
+  @Override
+  public RefDatabase getRefDatabase() {
+    return multiSiteRefDatabase;
+  }
+
+  @Override
+  public StoredConfig getConfig() {
+    return repository.getConfig();
+  }
+
+  @Override
+  public AttributesNodeProvider createAttributesNodeProvider() {
+    return repository.createAttributesNodeProvider();
+  }
+
+  @Override
+  public void scanForRepoChanges() throws IOException {
+    repository.scanForRepoChanges();
+  }
+
+  @Override
+  public void notifyIndexChanged(boolean b) {
+    repository.notifyIndexChanged(b);
+  }
+
+  @Override
+  public ReflogReader getReflogReader(String s) throws IOException {
+    return repository.getReflogReader(s);
+  }
+
+  @Override
+  public int hashCode() {
+    return repository.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    return repository.equals(obj);
+  }
+
+  @Override
+  public ListenerList getListenerList() {
+    return repository.getListenerList();
+  }
+
+  @Override
+  public void fireEvent(RepositoryEvent<?> event) {
+    repository.fireEvent(event);
+  }
+
+  @Override
+  public void create() throws IOException {
+    repository.create();
+  }
+
+  @Override
+  public File getDirectory() {
+    return repository.getDirectory();
+  }
+
+  @Override
+  public ObjectInserter newObjectInserter() {
+    return repository.newObjectInserter();
+  }
+
+  @Override
+  public ObjectReader newObjectReader() {
+    return repository.newObjectReader();
+  }
+
+  @Override
+  public FS getFS() {
+    return repository.getFS();
+  }
+
+  @Override
+  public boolean hasObject(AnyObjectId objectId) {
+    return repository.hasObject(objectId);
+  }
+
+  @Override
+  public ObjectLoader open(AnyObjectId objectId) throws MissingObjectException, IOException {
+    return repository.open(objectId);
+  }
+
+  @Override
+  public ObjectLoader open(AnyObjectId objectId, int typeHint)
+      throws MissingObjectException, IncorrectObjectTypeException, IOException {
+    return repository.open(objectId, typeHint);
+  }
+
+  @Override
+  public RefUpdate updateRef(String ref) throws IOException {
+    return multiSiteRefDatabase.wrapRefUpdate(repository.updateRef(ref));
+  }
+
+  @Override
+  public RefUpdate updateRef(String ref, boolean detach) throws IOException {
+    return multiSiteRefDatabase.wrapRefUpdate(repository.updateRef(ref, detach));
+  }
+
+  @Override
+  public RefRename renameRef(String fromRef, String toRef) throws IOException {
+    return repository.renameRef(fromRef, toRef);
+  }
+
+  @Override
+  public ObjectId resolve(String revstr)
+      throws AmbiguousObjectException, IncorrectObjectTypeException, RevisionSyntaxException,
+          IOException {
+    return repository.resolve(revstr);
+  }
+
+  @Override
+  public String simplify(String revstr) throws AmbiguousObjectException, IOException {
+    return repository.simplify(revstr);
+  }
+
+  @Override
+  public void incrementOpen() {
+    repository.incrementOpen();
+  }
+
+  @Override
+  public void close() {
+    repository.close();
+  }
+
+  @Override
+  public String toString() {
+    return repository.toString();
+  }
+
+  @Override
+  public String getFullBranch() throws IOException {
+    return repository.getFullBranch();
+  }
+
+  @Override
+  public String getBranch() throws IOException {
+    return repository.getBranch();
+  }
+
+  @Override
+  public Set<ObjectId> getAdditionalHaves() {
+    return repository.getAdditionalHaves();
+  }
+
+  @Override
+  public Map<String, Ref> getAllRefs() {
+    return repository.getAllRefs();
+  }
+
+  @Override
+  public Map<String, Ref> getTags() {
+    return repository.getTags();
+  }
+
+  @Override
+  public Ref peel(Ref ref) {
+    return repository.peel(ref);
+  }
+
+  @Override
+  public Map<AnyObjectId, Set<Ref>> getAllRefsByPeeledObjectId() {
+    return repository.getAllRefsByPeeledObjectId();
+  }
+
+  @Override
+  public File getIndexFile() throws NoWorkTreeException {
+    return repository.getIndexFile();
+  }
+
+  @Override
+  public RevCommit parseCommit(AnyObjectId id)
+      throws IncorrectObjectTypeException, IOException, MissingObjectException {
+    return repository.parseCommit(id);
+  }
+
+  @Override
+  public DirCache readDirCache() throws NoWorkTreeException, CorruptObjectException, IOException {
+    return repository.readDirCache();
+  }
+
+  @Override
+  public DirCache lockDirCache() throws NoWorkTreeException, CorruptObjectException, IOException {
+    return repository.lockDirCache();
+  }
+
+  @Override
+  public RepositoryState getRepositoryState() {
+    return repository.getRepositoryState();
+  }
+
+  @Override
+  public boolean isBare() {
+    return repository.isBare();
+  }
+
+  @Override
+  public File getWorkTree() throws NoWorkTreeException {
+    return repository.getWorkTree();
+  }
+
+  @Override
+  public String shortenRemoteBranchName(String refName) {
+    return repository.shortenRemoteBranchName(refName);
+  }
+
+  @Override
+  public String getRemoteName(String refName) {
+    return repository.getRemoteName(refName);
+  }
+
+  @Override
+  public String getGitwebDescription() throws IOException {
+    return repository.getGitwebDescription();
+  }
+
+  @Override
+  public void setGitwebDescription(String description) throws IOException {
+    repository.setGitwebDescription(description);
+  }
+
+  @Override
+  public String readMergeCommitMsg() throws IOException, NoWorkTreeException {
+    return repository.readMergeCommitMsg();
+  }
+
+  @Override
+  public void writeMergeCommitMsg(String msg) throws IOException {
+    repository.writeMergeCommitMsg(msg);
+  }
+
+  @Override
+  public String readCommitEditMsg() throws IOException, NoWorkTreeException {
+    return repository.readCommitEditMsg();
+  }
+
+  @Override
+  public void writeCommitEditMsg(String msg) throws IOException {
+    repository.writeCommitEditMsg(msg);
+  }
+
+  @Override
+  public List<ObjectId> readMergeHeads() throws IOException, NoWorkTreeException {
+    return repository.readMergeHeads();
+  }
+
+  @Override
+  public void writeMergeHeads(List<? extends ObjectId> heads) throws IOException {
+    repository.writeMergeHeads(heads);
+  }
+
+  @Override
+  public ObjectId readCherryPickHead() throws IOException, NoWorkTreeException {
+    return repository.readCherryPickHead();
+  }
+
+  @Override
+  public ObjectId readRevertHead() throws IOException, NoWorkTreeException {
+    return repository.readRevertHead();
+  }
+
+  @Override
+  public void writeCherryPickHead(ObjectId head) throws IOException {
+    repository.writeCherryPickHead(head);
+  }
+
+  @Override
+  public void writeRevertHead(ObjectId head) throws IOException {
+    repository.writeRevertHead(head);
+  }
+
+  @Override
+  public void writeOrigHead(ObjectId head) throws IOException {
+    repository.writeOrigHead(head);
+  }
+
+  @Override
+  public ObjectId readOrigHead() throws IOException, NoWorkTreeException {
+    return repository.readOrigHead();
+  }
+
+  @Override
+  public String readSquashCommitMsg() throws IOException {
+    return repository.readSquashCommitMsg();
+  }
+
+  @Override
+  public void writeSquashCommitMsg(String msg) throws IOException {
+    repository.writeSquashCommitMsg(msg);
+  }
+
+  @Override
+  public List<RebaseTodoLine> readRebaseTodo(String path, boolean includeComments)
+      throws IOException {
+    return repository.readRebaseTodo(path, includeComments);
+  }
+
+  @Override
+  public void writeRebaseTodoFile(String path, List<RebaseTodoLine> steps, boolean append)
+      throws IOException {
+    repository.writeRebaseTodoFile(path, steps, append);
+  }
+
+  @Override
+  public Set<String> getRemoteNames() {
+    return repository.getRemoteNames();
+  }
+
+  @Override
+  public void autoGC(ProgressMonitor monitor) {
+    repository.autoGC(monitor);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java
index f2a6b94..ea12e19 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java
@@ -14,18 +14,31 @@
 
 package com.googlesource.gerrit.plugins.multisite.validation;
 
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.server.git.validators.RefOperationValidationListener;
-import com.google.inject.AbstractModule;
-import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.NoOpDfsRefDatabase;
-import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefDatabase;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.googlesource.gerrit.plugins.multisite.Configuration;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.zookeeper.ZkValidationModule;
 
-public class ValidationModule extends AbstractModule {
+public class ValidationModule extends FactoryModule {
+  private final Configuration cfg;
+  private final boolean disableGitRepositoryValidation;
+
+  public ValidationModule(Configuration cfg, boolean disableGitRepositoryValidation) {
+    this.cfg = cfg;
+    this.disableGitRepositoryValidation = disableGitRepositoryValidation;
+  }
 
   @Override
   protected void configure() {
-    DynamicSet.bind(binder(), RefOperationValidationListener.class).to(InSyncChangeValidator.class);
+    factory(MultiSiteRepository.Factory.class);
+    factory(MultiSiteRefDatabase.Factory.class);
+    factory(MultiSiteRefUpdate.Factory.class);
+    factory(MultiSiteBatchRefUpdate.Factory.class);
 
-    bind(SharedRefDatabase.class).to(NoOpDfsRefDatabase.class);
+    if (!disableGitRepositoryValidation) {
+      bind(GitRepositoryManager.class).to(MultiSiteGitRepositoryManager.class);
+    }
+
+    install(new ZkValidationModule(cfg));
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/NoOpDfsRefDatabase.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/NoOpDfsRefDatabase.java
index 801fc3b..defa6ab 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/NoOpDfsRefDatabase.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/NoOpDfsRefDatabase.java
@@ -15,17 +15,11 @@
 package com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb;
 
 import java.io.IOException;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 
 public class NoOpDfsRefDatabase implements SharedRefDatabase {
 
   @Override
-  public Ref newRef(String refName, ObjectId objectId) {
-    return null;
-  }
-
-  @Override
   public boolean compareAndPut(String project, Ref oldRef, Ref newRef) throws IOException {
     return true;
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/SharedRefDatabase.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/SharedRefDatabase.java
index b995df9..87ebc96 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/SharedRefDatabase.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/SharedRefDatabase.java
@@ -16,6 +16,7 @@
 
 import java.io.IOException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
 import org.eclipse.jgit.lib.Ref;
 
 public interface SharedRefDatabase {
@@ -69,14 +70,16 @@
    * @param refName ref name
    * @param objectId object id
    */
-  Ref newRef(String refName, ObjectId objectId);
+  default Ref newRef(String refName, ObjectId objectId) {
+    return new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, refName, objectId);
+  }
 
   /**
    * Utility method for new refs.
    *
    * @param project project name of the ref
    * @param newRef new reference to store.
-   * @return
+   * @return true if the operation was successful; false otherwise.
    * @throws IOException
    */
   default boolean compareAndCreate(String project, Ref newRef) throws IOException {
@@ -113,4 +116,16 @@
    * @throws java.io.IOException the reference could not be removed due to a system error.
    */
   boolean compareAndRemove(String project, Ref oldRef) throws IOException;
+
+  /**
+   * Some references should not be stored in the SharedRefDatabase.
+   *
+   * @param refName
+   * @return true if it's to be ignore; false otherwise
+   */
+  default boolean ignoreRefInSharedDb(String refName) {
+    return refName == null
+        || refName.startsWith("refs/draft-comments")
+        || (refName.startsWith("refs/changes") && !refName.endsWith("/meta"));
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkSharedRefDatabase.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkSharedRefDatabase.java
new file mode 100644
index 0000000..503d93f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkSharedRefDatabase.java
@@ -0,0 +1,101 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.zookeeper;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.flogger.FluentLogger;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefDatabase;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import javax.inject.Named;
+import org.apache.curator.RetryPolicy;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.recipes.atomic.AtomicValue;
+import org.apache.curator.framework.recipes.atomic.DistributedAtomicValue;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+
+public class ZkSharedRefDatabase implements SharedRefDatabase {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final CuratorFramework client;
+  private final RetryPolicy retryPolicy;
+
+  @Inject
+  public ZkSharedRefDatabase(
+      CuratorFramework client, @Named("ZkLockRetryPolicy") RetryPolicy retryPolicy) {
+    this.client = client;
+    this.retryPolicy = retryPolicy;
+  }
+
+  @Override
+  public boolean compareAndRemove(String project, Ref oldRef) throws IOException {
+    return compareAndPut(project, oldRef, NULL_REF);
+  }
+
+  @Override
+  public boolean compareAndPut(String projectName, Ref oldRef, Ref newRef) throws IOException {
+    if (ignoreRefInSharedDb(MoreObjects.firstNonNull(oldRef.getName(), newRef.getName()))) {
+      return true;
+    }
+
+    final DistributedAtomicValue distributedRefValue =
+        new DistributedAtomicValue(client, pathFor(projectName, oldRef, newRef), retryPolicy);
+
+    try {
+      if (oldRef == NULL_REF) {
+        return distributedRefValue.initialize(writeObjectId(newRef.getObjectId()));
+      }
+      final ObjectId newValue =
+          newRef.getObjectId() == null ? ObjectId.zeroId() : newRef.getObjectId();
+      final AtomicValue<byte[]> newDistributedValue =
+          distributedRefValue.compareAndSet(
+              writeObjectId(oldRef.getObjectId()), writeObjectId(newValue));
+
+      if (!newDistributedValue.succeeded() && refNotInZk(projectName, oldRef, newRef)) {
+        return distributedRefValue.initialize(writeObjectId(newRef.getObjectId()));
+      }
+      return newDistributedValue.succeeded();
+    } catch (Exception e) {
+      logger.atWarning().withCause(e).log(
+          "Error trying to perform CAS at path %s", pathFor(projectName, oldRef, newRef));
+      throw new IOException(
+          String.format(
+              "Error trying to perform CAS at path %s", pathFor(projectName, oldRef, newRef)),
+          e);
+    }
+  }
+
+  private boolean refNotInZk(String projectName, Ref oldRef, Ref newRef) throws Exception {
+    return client.checkExists().forPath(pathFor(projectName, oldRef, newRef)) == null;
+  }
+
+  static String pathFor(String projectName, Ref oldRef, Ref newRef) {
+    return pathFor(projectName, MoreObjects.firstNonNull(oldRef.getName(), newRef.getName()));
+  }
+
+  static String pathFor(String projectName, String refName) {
+    return "/" + projectName + "/" + refName;
+  }
+
+  static ObjectId readObjectId(byte[] value) {
+    return ObjectId.fromString(value, 0);
+  }
+
+  static byte[] writeObjectId(ObjectId value) {
+    return ObjectId.toString(value).getBytes(StandardCharsets.US_ASCII);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkValidationModule.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkValidationModule.java
new file mode 100644
index 0000000..7cbb4b7
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkValidationModule.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.zookeeper;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.name.Names;
+import com.googlesource.gerrit.plugins.multisite.Configuration;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefDatabase;
+import org.apache.curator.RetryPolicy;
+import org.apache.curator.framework.CuratorFramework;
+
+public class ZkValidationModule extends AbstractModule {
+
+  private Configuration cfg;
+
+  public ZkValidationModule(Configuration cfg) {
+    this.cfg = cfg;
+  }
+
+  @Override
+  protected void configure() {
+    bind(SharedRefDatabase.class).to(ZkSharedRefDatabase.class);
+    bind(CuratorFramework.class).toInstance(cfg.getZookeeperConfig().buildCurator());
+    bind(RetryPolicy.class)
+        .annotatedWith(Names.named("ZkLockRetryPolicy"))
+        .toInstance(cfg.getZookeeperConfig().buildCasRetryPolicy());
+  }
+}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index b10d790..699ecff 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -2,8 +2,8 @@
 @PLUGIN@ Configuration
 =========================
 
-The @PLUGIN@ plugin must be installed on all the instances and the following fields
-should be specified in `$site_path/etc/@PLUGIN@.config` file:
+The @PLUGIN@ plugin must be installed on all the instances and the following
+fields should be specified in `$site_path/etc/@PLUGIN@.config` file:
 
 File '@PLUGIN@.config'
 --------------------
@@ -42,6 +42,18 @@
   cacheEventEnabled = true
   projectListEventEnabled = true
   streamEventEnabled = true
+
+[ref-database "zookeeper"]
+  connectString = "localhost:2181"
+  rootNode = "/gerrit/multi-site"
+  sessionTimeoutMs = 1000
+  connectionTimeoutMs = 1000
+  retryPolicyBaseSleepTimeMs = 1000
+  retryPolicyMaxSleepTimeMs = 3000
+  retryPolicyMaxRetries = 3
+  casRetryPolicyBaseSleepTimeMs = 100
+  casRetryPolicyMaxSleepTimeMs = 100
+  casRetryPolicyMaxRetries = 3
 ```
 
 ## Configuration parameters
@@ -51,13 +63,16 @@
     Defaults to true.
 
 ```cache.threadPoolSize```
-:   Maximum number of threads used to send cache evictions to the target instance.
+:   Maximum number of threads used to send cache evictions to the target
+    instance.
+
     Defaults to 4.
 
 ```cache.pattern```
 :   Pattern to match names of custom caches for which evictions should be
     forwarded (in addition to the core caches that are always forwarded). May be
     specified more than once to add multiple patterns.
+
     Defaults to an empty list, meaning only evictions of the core caches are
     forwarded.
 
@@ -79,9 +94,10 @@
 
 ```index.maxTries```
 :   Maximum number of times the plugin should attempt to reindex changes.
-    Setting this value to 0 will disable retries. After this number of failed tries,
-    an error is logged and the local index should be considered stale and needs
-    to be investigated and manually reindexed.
+    Setting this value to 0 will disable retries. After this number of failed
+    tries, an error is logged and the local index should be considered stale and
+    needs to be investigated and manually reindexed.
+
     Defaults to 2.
 
 ```index.retryInterval```
@@ -89,7 +105,8 @@
     Defaults to 30000 (30 seconds).
 
 ```kafka.bootstrapServers```
-:	List of Kafka broker hosts:port to use for publishing events to the message broker
+:	  List of Kafka broker hosts:port to use for publishing events to the message
+    broker
 
 ```kafka.indexEventTopic```
 :   Name of the Kafka topic to use for publishing indexing events
@@ -107,24 +124,28 @@
 :   Name of the Kafka topic to use for publishing cache eviction events
     Defaults to GERRIT.EVENT.PROJECT.LIST
 
-```kafka.publisher.enabled```
-:   Enable publishing events to Kafka
-    Defaults: false
-
 ```kafka.publisher.indexEventEnabled```
-:   Enable publication of index events, ignored when `kafka.publisher.enabled` is false
+:   Enable publication of index events, ignored when `kafka.publisher.enabled`
+    is false
+
     Defaults: true
 
 ```kafka.publisher.cacheEventEnabled```
-:   Enable publication of cache events, ignored when `kafka.publisher.enabled` is false
+:   Enable publication of cache events, ignored when `kafka.publisher.enabled`
+    is false
+
     Defaults: true
 
 ```kafka.publisher.projectListEventEnabled```
-:   Enable publication of project list events, ignored when `kafka.publisher.enabled` is false
+:   Enable publication of project list events, ignored when `kafka.publisher.enabled`
+    is false
+
     Defaults: true
 
-```kafka.publisher.streamEventEnabled```    
-:   Enable publication of stream events, ignored when `kafka.publisher.enabled` is false
+```kafka.publisher.streamEventEnabled```
+:   Enable publication of stream events, ignored when `kafka.publisher.enabled`
+    is false
+
     Defaults: true
 
 ```kafka.subscriber.enabled```
@@ -132,38 +153,115 @@
     Defaults: false
 
 ```kafka.subscriber.indexEventEnabled```
-:   Enable consumption of index events, ignored when `kafka.subscriber.enabled` is false
+:   Enable consumption of index events, ignored when `kafka.subscriber.enabled`
+    is false
+
     Defaults: true
 
 ```kafka.subscriber.cacheEventEnabled```
-:   Enable consumption of cache events, ignored when `kafka.subscriber.enabled` is false
+:   Enable consumption of cache events, ignored when `kafka.subscriber.enabled`
+    is false
+
     Defaults: true
 
 ```kafka.subscriber.projectListEventEnabled```
-:   Enable consumption of project list events, ignored when `kafka.subscriber.enabled` is false
+:   Enable consumption of project list events, ignored when `kafka.subscriber.enabled`
+    is false
+
     Defaults: true
 
-```kafka.subscriber.streamEventEnabled```    
-:   Enable consumption of stream events, ignored when `kafka.subscriber.enabled` is false
+```kafka.subscriber.streamEventEnabled```
+:   Enable consumption of stream events, ignored when `kafka.subscriber.enabled`
+    is false
+
     Defaults: true
 
 ```kafka.subscriber.pollingIntervalMs```
 :   Polling interval for checking incoming events
+
     Defaults: 1000
 
+```ref-database.zookeeper.connectString```
+:   Connection string to  zookeeper
+
+```ref-database.zookeeper.rootNode```
+:   Root node to use under Zookeeper to store/retrieve information
+
+    Defaults: "/gerrit/multi-site"
+
+
+```ref-database.zookeeper.sessionTimeoutMs```
+:   Root node to use under Zookeeper to store/retrieve information
+
+    Defaults: 1000
+
+```ref-database.zookeeper.connectionTimeoutMs```
+:   Root node to use under Zookeeper to store/retrieve information
+
+    Defaults: 1000
+
+```ref-database.zookeeper.retryPolicyBaseSleepTimeMs```
+:   Configuration for the base sleep timeout (iun ms) to use to create the
+    BoundedExponentialBackoffRetry policy used for the Zookeeper connection
+
+    Defaults: 1000
+
+```ref-database.zookeeper.retryPolicyMaxSleepTimeMs```
+:   Configuration for the max sleep timeout (iun ms) to use to create the
+    BoundedExponentialBackoffRetry policy used for the Zookeeper connection
+
+    Defaults: 3000
+
+```ref-database.zookeeper.retryPolicyMaxRetries```
+:   Configuration for the max number of retries to use to create the
+    BoundedExponentialBackoffRetry policy used for the Zookeeper connection
+
+    Defaults: 3
+
+```ref-database.zookeeper.casRetryPolicyBaseSleepTimeMs```
+:   Configuration for the base sleep timeout (iun ms) to use to create the
+    BoundedExponentialBackoffRetry policy used for the Compare and Swap
+    operations on Zookeeper
+
+    Defaults: 1000
+    
+```ref-database.zookeeper.casRetryPolicyMaxSleepTimeMs```
+:   Configuration for the max sleep timeout (iun ms) to use to create the
+    BoundedExponentialBackoffRetry policy used for the Compare and Swap
+    operations on Zookeeper
+
+    Defaults: 3000
+    
+```ref-database.zookeeper.casRetryPolicyMaxRetries```
+:   Configuration for the max number of retries to use to create the
+    BoundedExponentialBackoffRetry policy used for the Compare and Swap
+    operations on Zookeeper
+
+    Defaults: 3
+
+```ref-database.zookeeper.migrate```
+:   Set to true when the plugin has been applied to an already existing module
+    and there are no entries in Zookeeper for the existing refs. It will handle
+    update failures caused by the old refs not existing forcing the creation of
+    the new one
+
+    Defaults: false
+
 #### Custom kafka properties:
 
-In addition to the above settings, custom Kafka properties can be explicitly set for `publisher` and `subscriber`.
-In order to be acknowledged, these properties need to be prefixed with the `KafkaProp-` prefix and then camelCased,
-as follows: `KafkaProp-yourPropertyValue`
+In addition to the above settings, custom Kafka properties can be explicitly set
+for `publisher` and `subscriber`.
+In order to be acknowledged, these properties need to be prefixed with the
+`KafkaProp-` prefix and then camelCased, as follows: `KafkaProp-yourPropertyValue`
 
-For example, if you want to set the `auto.commit.interval.ms` property for your consumers, you will need to configure
-this property as `KafkaProp-autoCommitIntervalMs`.
+For example, if you want to set the `auto.commit.interval.ms` property for your
+consumers, you will need to configure this property as `KafkaProp-autoCommitIntervalMs`.
 
-**NOTE**: custom kafka properties will be ignored when the relevant subsection is disabled (i.e. `kafka.subscriber.enabled`
-and/or `kafka.publisher.enabled` are set to `false`).
+**NOTE**: custom kafka properties will be ignored when the relevant subsection is
+disabled (i.e. `kafka.subscriber.enabled` and/or `kafka.publisher.enabled` are
+set to `false`).
 
 The complete list of available settings can be found directly in the kafka website:
 
 * **Publisher**: https://kafka.apache.org/documentation/#producerconfigs
-* **Subscriber**: https://kafka.apache.org/documentation/#consumerconfigs
\ No newline at end of file
+* **Subscriber**: https://kafka.apache.org/documentation/#consumerconfigs
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/ConfigurationTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/ConfigurationTest.java
index a4bebb7..4ceead9 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/ConfigurationTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/ConfigurationTest.java
@@ -28,35 +28,31 @@
 import static com.googlesource.gerrit.plugins.multisite.Configuration.KafkaPublisher.KAFKA_PUBLISHER_SUBSECTION;
 import static com.googlesource.gerrit.plugins.multisite.Configuration.KafkaSubscriber.KAFKA_SUBSCRIBER_SUBSECTION;
 import static com.googlesource.gerrit.plugins.multisite.Configuration.THREAD_POOL_SIZE_KEY;
-import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.server.config.PluginConfigFactory;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
 @RunWith(MockitoJUnitRunner.class)
 public class ConfigurationTest {
   private static final String INVALID_BOOLEAN = "invalidBoolean";
   private static final String INVALID_INT = "invalidInt";
-  private static final String PLUGIN_NAME = "multi-site";
   private static final int THREAD_POOL_SIZE = 1;
 
-  @Mock private PluginConfigFactory pluginConfigFactoryMock;
   private Config globalPluginConfig;
+  private Config replicationConfig;
 
   @Before
   public void setUp() {
     globalPluginConfig = new Config();
-    when(pluginConfigFactoryMock.getGlobalPluginConfig(PLUGIN_NAME)).thenReturn(globalPluginConfig);
+    replicationConfig = new Config();
   }
 
   private Configuration getConfiguration() {
-    return new Configuration(pluginConfigFactoryMock, PLUGIN_NAME);
+    return new Configuration(globalPluginConfig, replicationConfig);
   }
 
   @Test
@@ -209,4 +205,11 @@
 
     assertThat(property).isNull();
   }
+
+  @Test
+  public void shouldReturnValidationErrorsWhenReplicationOnStartupIsEnabled() throws Exception {
+    Config replicationConfig = new Config();
+    replicationConfig.setBoolean("gerrit", null, "replicateOnStartup", true);
+    assertThat(new Configuration(globalPluginConfig, replicationConfig).validate()).isNotEmpty();
+  }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/ModuleTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/ModuleTest.java
index cf64a92..5281371 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/ModuleTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/ModuleTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.gerrit.server.config.SitePaths;
 import java.io.File;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -46,15 +47,19 @@
 
   @Test
   public void shouldGetInstanceId() throws Exception {
-    File tmpConfigDirectory = tempFolder.newFolder();
-    Path path = Paths.get(tmpConfigDirectory.getPath(), Configuration.INSTANCE_ID_FILE);
+    File tmpSitePath = tempFolder.newFolder();
+    File tmpPluginDataPath =
+        Paths.get(tmpSitePath.getPath(), "data", Configuration.PLUGIN_NAME).toFile();
+    tmpPluginDataPath.mkdirs();
+    Path path = Paths.get(tmpPluginDataPath.getPath(), Configuration.INSTANCE_ID_FILE);
+    SitePaths sitePaths = new SitePaths(Paths.get(tmpSitePath.getPath()));
     assertThat(path.toFile().exists()).isFalse();
 
-    UUID gotUUID1 = module.getInstanceId(Paths.get(tmpConfigDirectory.getPath()));
+    UUID gotUUID1 = module.getInstanceId(sitePaths);
     assertThat(gotUUID1).isNotNull();
     assertThat(path.toFile().exists()).isTrue();
 
-    UUID gotUUID2 = module.getInstanceId(Paths.get(tmpConfigDirectory.getPath()));
+    UUID gotUUID2 = module.getInstanceId(sitePaths);
     assertThat(gotUUID1).isEqualTo(gotUUID2);
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/cache/ProjectListUpdateHandlerTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/cache/ProjectListUpdateHandlerTest.java
index b2cc686..bf4b4cf 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/cache/ProjectListUpdateHandlerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/cache/ProjectListUpdateHandlerTest.java
@@ -36,7 +36,6 @@
 
 @RunWith(MockitoJUnitRunner.class)
 public class ProjectListUpdateHandlerTest {
-  private static final String PLUGIN_NAME = "multi-site";
 
   private ProjectListUpdateHandler handler;
 
@@ -44,9 +43,7 @@
 
   @Before
   public void setUp() {
-    handler =
-        new ProjectListUpdateHandler(
-            asDynamicSet(forwarder), MoreExecutors.directExecutor(), PLUGIN_NAME);
+    handler = new ProjectListUpdateHandler(asDynamicSet(forwarder), MoreExecutors.directExecutor());
   }
 
   private DynamicSet<ProjectListUpdateForwarder> asDynamicSet(
@@ -89,15 +86,11 @@
     ProjectListUpdateTask task =
         handler.new ProjectListUpdateTask(new ProjectListUpdateEvent(projectName, false));
     assertThat(task.toString())
-        .isEqualTo(
-            String.format(
-                "[%s] Update project list in target instance: add '%s'", PLUGIN_NAME, projectName));
+        .isEqualTo(String.format("Update project list in target instance: add '%s'", projectName));
 
     task = handler.new ProjectListUpdateTask(new ProjectListUpdateEvent(projectName, true));
     assertThat(task.toString())
         .isEqualTo(
-            String.format(
-                "[%s] Update project list in target instance: remove '%s'",
-                PLUGIN_NAME, projectName));
+            String.format("Update project list in target instance: remove '%s'", projectName));
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/event/EventHandlerTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/event/EventHandlerTest.java
index bbef652..990a03f 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/event/EventHandlerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/event/EventHandlerTest.java
@@ -35,7 +35,6 @@
 
 @RunWith(MockitoJUnitRunner.class)
 public class EventHandlerTest {
-  private static final String PLUGIN_NAME = "multi-site";
 
   private EventHandler eventHandler;
 
@@ -43,8 +42,7 @@
 
   @Before
   public void setUp() {
-    eventHandler =
-        new EventHandler(asDynamicSet(forwarder), MoreExecutors.directExecutor(), PLUGIN_NAME);
+    eventHandler = new EventHandler(asDynamicSet(forwarder), MoreExecutors.directExecutor());
   }
 
   private DynamicSet<StreamEventForwarder> asDynamicSet(StreamEventForwarder forwarder) {
@@ -79,7 +77,6 @@
     Event event = new RefUpdatedEvent();
     EventTask task = eventHandler.new EventTask(event);
     assertThat(task.toString())
-        .isEqualTo(
-            String.format("[%s] Send event '%s' to target instance", PLUGIN_NAME, event.type));
+        .isEqualTo(String.format("Send event '%s' to target instance", event.type));
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedAwareEventBrokerTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedAwareEventBrokerTest.java
deleted file mode 100644
index 5ccf4ab..0000000
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedAwareEventBrokerTest.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed 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 com.googlesource.gerrit.plugins.multisite.forwarder;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.server.events.Event;
-import com.google.gerrit.server.events.EventListener;
-import com.google.gerrit.server.plugincontext.PluginContext.PluginMetrics;
-import com.google.gerrit.server.plugincontext.PluginSetContext;
-import org.junit.Before;
-import org.junit.Test;
-
-public class ForwardedAwareEventBrokerTest {
-
-  private EventListener listenerMock;
-  private ForwardedAwareEventBroker broker;
-  private Event event = new Event(null) {};
-
-  @Before
-  public void setUp() {
-    PluginMetrics mockMetrics = mock(PluginMetrics.class);
-    listenerMock = mock(EventListener.class);
-    DynamicSet<EventListener> set = DynamicSet.emptySet();
-    set.add("multi-site", listenerMock);
-    PluginSetContext<EventListener> listeners = new PluginSetContext<>(set, mockMetrics);
-    broker = new ForwardedAwareEventBroker(null, listeners, null, null, null);
-  }
-
-  @Test
-  public void shouldDispatchEvent() {
-    broker.fireEventForUnrestrictedListeners(event);
-    verify(listenerMock).onEvent(event);
-  }
-
-  @Test
-  public void shouldNotDispatchForwardedEvents() {
-    Context.setForwardedEvent(true);
-    try {
-      broker.fireEventForUnrestrictedListeners(event);
-    } finally {
-      Context.unsetForwardedEvent();
-    }
-    verifyZeroInteractions(listenerMock);
-  }
-}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/EventConsumerIT.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/EventConsumerIT.java
index 74cb04b..5d64d28 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/EventConsumerIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/EventConsumerIT.java
@@ -17,26 +17,31 @@
 import static com.google.common.truth.Truth.assertThat;
 import static java.util.stream.Collectors.toSet;
 
-import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.LogThreshold;
 import com.google.gerrit.acceptance.NoHttpd;
-import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.data.PatchSetAttribute;
 import com.google.gerrit.server.events.CommentAddedEvent;
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gson.Gson;
+import com.google.inject.Inject;
 import com.google.inject.Key;
 import com.google.inject.TypeLiteral;
 import com.googlesource.gerrit.plugins.multisite.Configuration;
 import com.googlesource.gerrit.plugins.multisite.Module;
-import com.googlesource.gerrit.plugins.multisite.broker.GsonProvider;
+import com.googlesource.gerrit.plugins.multisite.broker.BrokerGson;
 import com.googlesource.gerrit.plugins.multisite.forwarder.events.ChangeIndexEvent;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
@@ -49,16 +54,18 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
 import org.junit.Test;
 import org.testcontainers.containers.KafkaContainer;
 
 @NoHttpd
 @LogThreshold(level = "INFO")
-@TestPlugin(
-    name = "multi-site",
-    sysModule =
-        "com.googlesource.gerrit.plugins.multisite.kafka.consumer.EventConsumerIT$KafkaTestContainerModule")
-public class EventConsumerIT extends LightweightPluginDaemonTest {
+@UseLocalDisk
+public class EventConsumerIT extends AbstractDaemonTest {
+  public static final String GERRIT_CONFIG_KEY = "gerrit.installModule";
+  public static final String GERRIT_CONFIG_VALUE =
+      "com.googlesource.gerrit.plugins.multisite.kafka.consumer.EventConsumerIT$KafkaTestContainerModule";
   private static final int QUEUE_POLL_TIMEOUT_MSECS = 10000;
 
   public static class KafkaTestContainerModule extends LifecycleModule {
@@ -81,24 +88,51 @@
       }
     }
 
+    private final FileBasedConfig config;
+    private final Module multiSiteModule;
+
+    @Inject
+    public KafkaTestContainerModule(SitePaths sitePaths) {
+      this.config =
+          new FileBasedConfig(
+              sitePaths.etc_dir.resolve(Configuration.MULTI_SITE_CONFIG).toFile(), FS.DETECTED);
+      this.multiSiteModule = new Module(new Configuration(config, new Config()), true);
+    }
+
     @Override
     protected void configure() {
-      final KafkaContainer kafka = new KafkaContainer();
-      kafka.start();
+      try {
+        final KafkaContainer kafka = startAndConfigureKafkaConnection();
 
-      Config config = new Config();
-      config.setString("kafka", null, "bootstrapServers", kafka.getBootstrapServers());
+        listener().toInstance(new KafkaStopAtShutdown(kafka));
+
+        install(multiSiteModule);
+
+      } catch (IOException e) {
+        throw new IllegalStateException(e);
+      }
+    }
+
+    private KafkaContainer startAndConfigureKafkaConnection() throws IOException {
+      KafkaContainer kafkaContainer = new KafkaContainer();
+      kafkaContainer.start();
+
+      config.setString("kafka", null, "bootstrapServers", kafkaContainer.getBootstrapServers());
       config.setBoolean("kafka", "publisher", "enabled", true);
       config.setBoolean("kafka", "subscriber", "enabled", true);
-      Configuration multiSiteConfig = new Configuration(config);
+      config.setBoolean("ref-database", null, "enabled", false);
+      config.save();
+      Configuration multiSiteConfig = new Configuration(config, new Config());
       bind(Configuration.class).toInstance(multiSiteConfig);
-      install(new Module(multiSiteConfig));
 
-      listener().toInstance(new KafkaStopAtShutdown(kafka));
+      listener().toInstance(new KafkaStopAtShutdown(kafkaContainer));
+
+      return kafkaContainer;
     }
   }
 
   @Test
+  @GerritConfig(name = GERRIT_CONFIG_KEY, value = GERRIT_CONFIG_VALUE)
   public void createChangeShouldPropagateChangeIndexAndRefUpdateStreamEvent() throws Exception {
     LinkedBlockingQueue<SourceAwareEventWrapper> droppedEventsQueue = captureDroppedEvents();
     drainQueue(droppedEventsQueue);
@@ -112,18 +146,18 @@
     String patchsetRef = change.currentPatchSet().getRefName();
 
     Map<String, List<Event>> eventsByType = receiveEventsByType(droppedEventsQueue);
+    assertThat(eventsByType).isNotEmpty();
+
     assertThat(eventsByType.get("change-index"))
         .containsExactly(createChangeIndexEvent(project, changeNum, getParentCommit(change)));
 
     assertThat(
-            eventsByType
-                .get("ref-updated")
-                .stream()
+            eventsByType.get("ref-updated").stream()
                 .map(e -> ((RefUpdatedEvent) e).getRefName())
                 .collect(toSet()))
-        .containsAllOf(
-            changeNotesRef,
-            patchsetRef); // 'refs/sequences/changes' not always updated thus not checked
+        .containsAllOf(changeNotesRef, patchsetRef); // 'refs/sequences/changes'
+    // not always updated thus
+    // not checked
 
     List<Event> patchSetCreatedEvents = eventsByType.get("patchset-created");
     assertThat(patchSetCreatedEvents).hasSize(1);
@@ -146,6 +180,7 @@
   }
 
   @Test
+  @GerritConfig(name = GERRIT_CONFIG_KEY, value = GERRIT_CONFIG_VALUE)
   public void reviewChangeShouldPropagateChangeIndexAndCommentAdded() throws Exception {
     LinkedBlockingQueue<SourceAwareEventWrapper> droppedEventsQueue = captureDroppedEvents();
     ChangeData change = createChange().getChange();
@@ -159,6 +194,8 @@
 
     Map<String, List<Event>> eventsByType = receiveEventsByType(droppedEventsQueue);
 
+    assertThat(eventsByType).isNotEmpty();
+
     assertThat(eventsByType.get("change-index"))
         .containsExactly(createChangeIndexEvent(project, changeNum, getParentCommit(change)));
 
@@ -191,8 +228,8 @@
 
     TypeLiteral<DynamicSet<DroppedEventListener>> type =
         new TypeLiteral<DynamicSet<DroppedEventListener>>() {};
-    plugin
-        .getSysInjector()
+    server
+        .getTestInjector()
         .getInstance(Key.get(type))
         .add(
             "multi-site",
@@ -207,19 +244,18 @@
 
   private Map<String, List<Event>> receiveEventsByType(
       LinkedBlockingQueue<SourceAwareEventWrapper> queue) throws InterruptedException {
-    return drainQueue(queue)
-        .stream()
+    return drainQueue(queue).stream()
         .sorted(Comparator.comparing(e -> e.type))
         .collect(Collectors.groupingBy(e -> e.type));
   }
 
   private List<Event> drainQueue(LinkedBlockingQueue<SourceAwareEventWrapper> queue)
       throws InterruptedException {
-    GsonProvider gsonProvider = plugin.getSysInjector().getInstance(Key.get(GsonProvider.class));
+    Gson gson = server.getTestInjector().getInstance(Key.get(Gson.class, BrokerGson.class));
     SourceAwareEventWrapper event;
     List<Event> eventsList = new ArrayList<>();
     while ((event = queue.poll(QUEUE_POLL_TIMEOUT_MSECS, TimeUnit.MILLISECONDS)) != null) {
-      eventsList.add(event.getEventBody(gsonProvider));
+      eventsList.add(event.getEventBody(gson));
     }
     return eventsList;
   }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/KafkaEventDeserializerTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/KafkaEventDeserializerTest.java
index 4a3b466..ce958cf 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/KafkaEventDeserializerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/KafkaEventDeserializerTest.java
@@ -17,7 +17,6 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gson.Gson;
-import com.google.inject.Provider;
 import com.googlesource.gerrit.plugins.multisite.broker.GsonProvider;
 import java.util.UUID;
 import org.junit.Before;
@@ -28,8 +27,8 @@
 
   @Before
   public void setUp() {
-    final Provider<Gson> gsonProvider = new GsonProvider();
-    deserializer = new KafkaEventDeserializer(gsonProvider);
+    final Gson gson = new GsonProvider().get();
+    deserializer = new KafkaEventDeserializer(gson);
   }
 
   @Test
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/InSyncChangeValidatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/InSyncChangeValidatorTest.java
deleted file mode 100644
index 6668529..0000000
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/InSyncChangeValidatorTest.java
+++ /dev/null
@@ -1,331 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.hamcrest.CoreMatchers.nullValue;
-import static org.hamcrest.CoreMatchers.sameInstance;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.argThat;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.lenient;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.events.RefReceivedEvent;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.validators.ValidationMessage;
-import com.google.gerrit.server.validators.ValidationException;
-import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefDatabase;
-import java.io.IOException;
-import java.util.List;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.transport.ReceiveCommand.Type;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentMatcher;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
-
-@RunWith(MockitoJUnitRunner.class)
-public class InSyncChangeValidatorTest {
-  static final String PROJECT_NAME = "AProject";
-  static final Project.NameKey PROJECT_NAMEKEY = new Project.NameKey(PROJECT_NAME);
-  static final String REF_NAME = "refs/heads/master";
-  static final String REF_PATCHSET_NAME = "refs/changes/45/1245/1";
-  static final String REF_PATCHSET_META_NAME = "refs/changes/45/1245/1/meta";
-  static final ObjectId REF_OBJID = ObjectId.fromString("f2ffe80abb77223f3f8921f3f068b0e32d40f798");
-  static final ObjectId REF_OBJID_OLD =
-      ObjectId.fromString("a9a7a6fd1e9ad39a13fef5e897dc6d932a3282e1");
-  static final ReceiveCommand RECEIVE_COMMAND_CREATE_REF =
-      new ReceiveCommand(ObjectId.zeroId(), REF_OBJID, REF_NAME, Type.CREATE);
-  static final ReceiveCommand RECEIVE_COMMAND_UPDATE_REF =
-      new ReceiveCommand(REF_OBJID_OLD, REF_OBJID, REF_NAME, Type.UPDATE);
-  static final ReceiveCommand RECEIVE_COMMAND_DELETE_REF =
-      new ReceiveCommand(REF_OBJID_OLD, ObjectId.zeroId(), REF_NAME, Type.DELETE);
-  static final ReceiveCommand RECEIVE_COMMAND_CREATE_PATCHSET_REF =
-      new ReceiveCommand(ObjectId.zeroId(), REF_OBJID, REF_PATCHSET_NAME, Type.CREATE);
-  static final ReceiveCommand RECEIVE_COMMAND_CREATE_PATCHSET_META_REF =
-      new ReceiveCommand(ObjectId.zeroId(), REF_OBJID, REF_PATCHSET_META_NAME, Type.CREATE);
-
-  @Rule public ExpectedException expectedException = ExpectedException.none();
-
-  @Mock SharedRefDatabase dfsRefDatabase;
-
-  @Mock Repository repo;
-
-  @Mock RefDatabase localRefDatabase;
-
-  @Mock GitRepositoryManager repoManager;
-
-  private InSyncChangeValidator validator;
-
-  static class TestRef implements Ref {
-    private final String name;
-    private final ObjectId objectId;
-
-    public TestRef(String name, ObjectId objectId) {
-      super();
-      this.name = name;
-      this.objectId = objectId;
-    }
-
-    @Override
-    public String getName() {
-      return name;
-    }
-
-    @Override
-    public boolean isSymbolic() {
-      return false;
-    }
-
-    @Override
-    public Ref getLeaf() {
-      return null;
-    }
-
-    @Override
-    public Ref getTarget() {
-      return null;
-    }
-
-    @Override
-    public ObjectId getObjectId() {
-      return objectId;
-    }
-
-    @Override
-    public ObjectId getPeeledObjectId() {
-      return null;
-    }
-
-    @Override
-    public boolean isPeeled() {
-      return false;
-    }
-
-    @Override
-    public Storage getStorage() {
-      return Storage.LOOSE;
-    }
-  }
-
-  static class RefMatcher implements ArgumentMatcher<Ref> {
-    private final String name;
-    private final ObjectId objectId;
-
-    public RefMatcher(String name, ObjectId objectId) {
-      super();
-      this.name = name;
-      this.objectId = objectId;
-    }
-
-    @Override
-    public boolean matches(Ref that) {
-      if (that == null) {
-        return false;
-      }
-
-      return name.equals(that.getName()) && objectId.equals(that.getObjectId());
-    }
-  }
-
-  public static Ref eqRef(String name, ObjectId objectId) {
-    return argThat(new RefMatcher(name, objectId));
-  }
-
-  Ref testRef = new TestRef(REF_NAME, REF_OBJID);
-  RefReceivedEvent testRefReceivedEvent =
-      new RefReceivedEvent() {
-
-        @Override
-        public String getRefName() {
-          return command.getRefName();
-        }
-
-        @Override
-        public com.google.gerrit.reviewdb.client.Project.NameKey getProjectNameKey() {
-          return PROJECT_NAMEKEY;
-        }
-      };
-
-  @Before
-  public void setUp() throws IOException {
-    doReturn(testRef).when(dfsRefDatabase).newRef(REF_NAME, REF_OBJID);
-    doReturn(repo).when(repoManager).openRepository(PROJECT_NAMEKEY);
-    doReturn(localRefDatabase).when(repo).getRefDatabase();
-    lenient()
-        .doThrow(new NullPointerException("oldRef is null"))
-        .when(dfsRefDatabase)
-        .compareAndPut(any(), eq(null), any());
-    lenient()
-        .doThrow(new NullPointerException("newRef is null"))
-        .when(dfsRefDatabase)
-        .compareAndPut(any(), any(), eq(null));
-    lenient()
-        .doThrow(new NullPointerException("project name is null"))
-        .when(dfsRefDatabase)
-        .compareAndPut(eq(null), any(), any());
-
-    validator = new InSyncChangeValidator(dfsRefDatabase, repoManager);
-    repoManager.createRepository(PROJECT_NAMEKEY);
-  }
-
-  @Test
-  public void shouldNotVerifyStatusOfImmutablePatchSetRefs() throws Exception {
-    testRefReceivedEvent.command = RECEIVE_COMMAND_CREATE_PATCHSET_REF;
-    final List<ValidationMessage> validationMessages =
-        validator.onRefOperation(testRefReceivedEvent);
-
-    assertThat(validationMessages).isEmpty();
-
-    verifyZeroInteractions(dfsRefDatabase);
-  }
-
-  @Test
-  public void shouldVerifyStatusOfPatchSetMetaRefs() throws Exception {
-    testRefReceivedEvent.command = RECEIVE_COMMAND_CREATE_PATCHSET_META_REF;
-
-    Ref testRefMeta = new TestRef(REF_PATCHSET_META_NAME, REF_OBJID);
-    doReturn(testRefMeta).when(dfsRefDatabase).newRef(REF_PATCHSET_META_NAME, REF_OBJID);
-
-    validator.onRefOperation(testRefReceivedEvent);
-
-    verify(dfsRefDatabase)
-        .compareAndCreate(eq(PROJECT_NAME), eqRef(REF_PATCHSET_META_NAME, REF_OBJID));
-  }
-
-  @Test
-  public void shouldInsertNewRefInDfsDatabaseWhenHandlingRefCreationEvents() throws Exception {
-    testRefReceivedEvent.command = RECEIVE_COMMAND_CREATE_REF;
-
-    final List<ValidationMessage> validationMessages =
-        validator.onRefOperation(testRefReceivedEvent);
-
-    assertThat(validationMessages).isEmpty();
-    verify(dfsRefDatabase).compareAndCreate(eq(PROJECT_NAME), eqRef(REF_NAME, REF_OBJID));
-  }
-
-  @Test
-  public void shouldFailRefCreationIfInsertANewRefInDfsDatabaseFails() throws Exception {
-    testRefReceivedEvent.command = RECEIVE_COMMAND_CREATE_REF;
-
-    IllegalArgumentException alreadyInDb = new IllegalArgumentException("obj is already in db");
-
-    doThrow(alreadyInDb)
-        .when(dfsRefDatabase)
-        .compareAndCreate(eq(PROJECT_NAME), eqRef(REF_NAME, REF_OBJID));
-
-    expectedException.expect(ValidationException.class);
-    expectedException.expectCause(sameInstance(alreadyInDb));
-
-    validator.onRefOperation(testRefReceivedEvent);
-  }
-
-  @Test
-  public void shouldUpdateRefInDfsDatabaseWhenHandlingRefUpdateEvents() throws Exception {
-    testRefReceivedEvent.command = RECEIVE_COMMAND_UPDATE_REF;
-    doReturn(new TestRef(REF_NAME, REF_OBJID_OLD)).when(localRefDatabase).getRef(REF_NAME);
-    doReturn(true)
-        .when(dfsRefDatabase)
-        .compareAndPut(
-            eq(PROJECT_NAME), eqRef(REF_NAME, REF_OBJID_OLD), eqRef(REF_NAME, REF_OBJID));
-
-    final List<ValidationMessage> validationMessages =
-        validator.onRefOperation(testRefReceivedEvent);
-
-    assertThat(validationMessages).isEmpty();
-    verify(dfsRefDatabase)
-        .compareAndPut(
-            eq(PROJECT_NAME), eqRef(REF_NAME, REF_OBJID_OLD), eqRef(REF_NAME, REF_OBJID));
-  }
-
-  @Test
-  public void shouldFailRefUpdateIfRefUpdateInDfsRefDatabaseReturnsFalse() throws Exception {
-    testRefReceivedEvent.command = RECEIVE_COMMAND_UPDATE_REF;
-    doReturn(new TestRef(REF_NAME, REF_OBJID_OLD)).when(localRefDatabase).getRef(REF_NAME);
-    doReturn(false)
-        .when(dfsRefDatabase)
-        .compareAndPut(
-            eq(PROJECT_NAME), eqRef(REF_NAME, REF_OBJID_OLD), eqRef(REF_NAME, REF_OBJID));
-    expectedException.expect(ValidationException.class);
-    expectedException.expectCause(nullValue(Exception.class));
-
-    validator.onRefOperation(testRefReceivedEvent);
-  }
-
-  @Test
-  public void shouldFailRefUpdateIfRefIsNotInDfsRefDatabase() throws Exception {
-    testRefReceivedEvent.command = RECEIVE_COMMAND_UPDATE_REF;
-    doReturn(null).when(localRefDatabase).getRef(REF_NAME);
-
-    expectedException.expect(ValidationException.class);
-    expectedException.expectCause(nullValue(Exception.class));
-
-    validator.onRefOperation(testRefReceivedEvent);
-  }
-
-  @Test
-  public void shouldDeleteRefInDfsDatabaseWhenHandlingRefDeleteEvents() throws Exception {
-    testRefReceivedEvent.command = RECEIVE_COMMAND_DELETE_REF;
-    doReturn(new TestRef(REF_NAME, REF_OBJID_OLD)).when(localRefDatabase).getRef(REF_NAME);
-    doReturn(true)
-        .when(dfsRefDatabase)
-        .compareAndRemove(eq(PROJECT_NAME), eqRef(REF_NAME, REF_OBJID_OLD));
-
-    final List<ValidationMessage> validationMessages =
-        validator.onRefOperation(testRefReceivedEvent);
-
-    assertThat(validationMessages).isEmpty();
-
-    verify(dfsRefDatabase).compareAndRemove(eq(PROJECT_NAME), eqRef(REF_NAME, REF_OBJID_OLD));
-  }
-
-  @Test
-  public void shouldFailRefDeletionIfRefDeletionInDfsRefDatabaseReturnsFalse() throws Exception {
-    testRefReceivedEvent.command = RECEIVE_COMMAND_DELETE_REF;
-    doReturn(new TestRef(REF_NAME, REF_OBJID_OLD)).when(localRefDatabase).getRef(REF_NAME);
-    doReturn(false)
-        .when(dfsRefDatabase)
-        .compareAndRemove(eq(PROJECT_NAME), eqRef(REF_NAME, REF_OBJID_OLD));
-
-    expectedException.expect(ValidationException.class);
-    expectedException.expectCause(nullValue(Exception.class));
-
-    validator.onRefOperation(testRefReceivedEvent);
-  }
-
-  @Test
-  public void shouldFailRefDeletionIfRefIsNotInDfsDatabase() throws Exception {
-    testRefReceivedEvent.command = RECEIVE_COMMAND_DELETE_REF;
-    doReturn(null).when(localRefDatabase).getRef(REF_NAME);
-
-    expectedException.expect(ValidationException.class);
-    expectedException.expectCause(nullValue(Exception.class));
-
-    validator.onRefOperation(testRefReceivedEvent);
-  }
-}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteBatchRefUpdateTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteBatchRefUpdateTest.java
new file mode 100644
index 0000000..bc558d5
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteBatchRefUpdateTest.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed 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.
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation;
+
+import static org.mockito.Mockito.doReturn;
+
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefDatabase;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.zookeeper.RefFixture;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class MultiSiteBatchRefUpdateTest implements RefFixture {
+
+  @Mock SharedRefDatabase sharedRefDb;
+  @Mock BatchRefUpdate batchRefUpdate;
+  @Mock RefDatabase refDatabase;
+  @Mock RevWalk revWalk;
+  @Mock ProgressMonitor progressMonitor;
+
+  private final Ref oldRef =
+      new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, A_TEST_REF_NAME, AN_OBJECT_ID_1);
+  private final Ref newRef =
+      new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, A_TEST_REF_NAME, AN_OBJECT_ID_2);
+  ReceiveCommand receiveCommand =
+      new ReceiveCommand(oldRef.getObjectId(), newRef.getObjectId(), oldRef.getName());
+
+  private MultiSiteBatchRefUpdate multiSiteRefUpdate;
+
+  @Rule public TestName nameRule = new TestName();
+
+  @Override
+  public String testBranch() {
+    return "branch_" + nameRule.getMethodName();
+  }
+
+  private void setMockRequiredReturnValues() throws IOException {
+    doReturn(batchRefUpdate).when(refDatabase).newBatchUpdate();
+    doReturn(Arrays.asList(receiveCommand)).when(batchRefUpdate).getCommands();
+    doReturn(oldRef).when(refDatabase).getRef(A_TEST_REF_NAME);
+    doReturn(newRef).when(sharedRefDb).newRef(A_TEST_REF_NAME, AN_OBJECT_ID_2);
+
+    multiSiteRefUpdate = new MultiSiteBatchRefUpdate(sharedRefDb, A_TEST_PROJECT_NAME, refDatabase);
+  }
+
+  @Test
+  public void executeAndDelegateSuccessfullyWithNoExceptions() throws IOException {
+    setMockRequiredReturnValues();
+
+    // When compareAndPut against sharedDb succeeds
+    doReturn(true).when(sharedRefDb).compareAndPut(A_TEST_PROJECT_NAME, oldRef, newRef);
+    multiSiteRefUpdate.execute(revWalk, progressMonitor, Collections.emptyList());
+  }
+
+  @Test(expected = IOException.class)
+  public void executeAndFailsWithExceptions() throws IOException {
+    setMockRequiredReturnValues();
+
+    // When compareAndPut against sharedDb fails
+    doReturn(false).when(sharedRefDb).compareAndPut(A_TEST_PROJECT_NAME, oldRef, newRef);
+    multiSiteRefUpdate.execute(revWalk, progressMonitor, Collections.emptyList());
+  }
+
+  @Test
+  public void executeSuccessfullyWithNoExceptionsWhenEmptyList() throws IOException {
+    doReturn(batchRefUpdate).when(refDatabase).newBatchUpdate();
+    doReturn(Collections.emptyList()).when(batchRefUpdate).getCommands();
+
+    multiSiteRefUpdate = new MultiSiteBatchRefUpdate(sharedRefDb, A_TEST_PROJECT_NAME, refDatabase);
+
+    multiSiteRefUpdate.execute(revWalk, progressMonitor, Collections.emptyList());
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteGitRepositoryManagerTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteGitRepositoryManagerTest.java
new file mode 100644
index 0000000..87b8719
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteGitRepositoryManagerTest.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed 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.
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.zookeeper.RefFixture;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class MultiSiteGitRepositoryManagerTest implements RefFixture {
+
+  @Mock LocalDiskRepositoryManager localDiskRepositoryManagerMock;
+
+  @Mock MultiSiteRepository.Factory multiSiteRepositoryFactoryMock;
+
+  @Mock Repository repositoryMock;
+
+  @Mock MultiSiteRepository multiSiteRepositoryMock;
+
+  MultiSiteGitRepositoryManager msRepoMgr;
+
+  @Override
+  public String testBranch() {
+    return "foo";
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    doReturn(multiSiteRepositoryMock)
+        .when(multiSiteRepositoryFactoryMock)
+        .create(A_TEST_PROJECT_NAME, repositoryMock);
+    msRepoMgr =
+        new MultiSiteGitRepositoryManager(
+            multiSiteRepositoryFactoryMock, localDiskRepositoryManagerMock);
+  }
+
+  @Test
+  public void openRepositoryShouldCreateMultiSiteRepositoryWrapper() throws Exception {
+    doReturn(repositoryMock)
+        .when(localDiskRepositoryManagerMock)
+        .openRepository(A_TEST_PROJECT_NAME_KEY);
+
+    msRepoMgr.openRepository(A_TEST_PROJECT_NAME_KEY);
+
+    verifyThatMultiSiteRepositoryWrapperHasBeenCreated();
+  }
+
+  @Test
+  public void createRepositoryShouldCreateMultiSiteRepositoryWrapper() throws Exception {
+    doReturn(repositoryMock)
+        .when(localDiskRepositoryManagerMock)
+        .createRepository(A_TEST_PROJECT_NAME_KEY);
+
+    msRepoMgr.createRepository(A_TEST_PROJECT_NAME_KEY);
+
+    verifyThatMultiSiteRepositoryWrapperHasBeenCreated();
+  }
+
+  private void verifyThatMultiSiteRepositoryWrapperHasBeenCreated() {
+    verify(multiSiteRepositoryFactoryMock).create(A_TEST_PROJECT_NAME, repositoryMock);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRefDatabaseTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRefDatabaseTest.java
new file mode 100644
index 0000000..9916199
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRefDatabaseTest.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed 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.
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.zookeeper.RefFixture;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class MultiSiteRefDatabaseTest implements RefFixture {
+
+  @Rule public TestName nameRule = new TestName();
+
+  @Mock MultiSiteRefUpdate.Factory refUpdateFactoryMock;
+  @Mock MultiSiteBatchRefUpdate.Factory refBatchUpdateFactoryMock;
+
+  @Mock RefDatabase refDatabaseMock;
+
+  @Mock RefUpdate refUpdateMock;
+
+  @Override
+  public String testBranch() {
+    return "branch_" + nameRule.getMethodName();
+  }
+
+  @Test
+  public void newUpdateShouldCreateMultiSiteRefUpdate() throws Exception {
+    String refName = aBranchRef();
+    MultiSiteRefDatabase multiSiteRefDb =
+        new MultiSiteRefDatabase(
+            refUpdateFactoryMock, refBatchUpdateFactoryMock, A_TEST_PROJECT_NAME, refDatabaseMock);
+    doReturn(refUpdateMock).when(refDatabaseMock).newUpdate(refName, false);
+
+    multiSiteRefDb.newUpdate(refName, false);
+
+    verify(refUpdateFactoryMock).create(A_TEST_PROJECT_NAME, refUpdateMock);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRefUpdateTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRefUpdateTest.java
new file mode 100644
index 0000000..9b8acde
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRefUpdateTest.java
@@ -0,0 +1,122 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed 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.
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doReturn;
+
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefDatabase;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.zookeeper.RefFixture;
+import java.io.IOException;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class MultiSiteRefUpdateTest implements RefFixture {
+
+  @Mock SharedRefDatabase sharedRefDb;
+  @Mock RefUpdate refUpdate;
+  private final Ref oldRef =
+      new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, A_TEST_REF_NAME, AN_OBJECT_ID_1);
+  private final Ref newRef =
+      new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, A_TEST_REF_NAME, AN_OBJECT_ID_2);
+
+  @Rule public TestName nameRule = new TestName();
+
+  @Override
+  public String testBranch() {
+    return "branch_" + nameRule.getMethodName();
+  }
+
+  private void setMockRequiredReturnValues() {
+    doReturn(oldRef).when(refUpdate).getRef();
+    doReturn(A_TEST_REF_NAME).when(refUpdate).getName();
+    doReturn(AN_OBJECT_ID_2).when(refUpdate).getNewObjectId();
+    doReturn(newRef).when(sharedRefDb).newRef(A_TEST_REF_NAME, AN_OBJECT_ID_2);
+  }
+
+  @Test
+  public void newUpdateShouldValidateAndSucceed() throws IOException {
+    setMockRequiredReturnValues();
+
+    // When compareAndPut succeeds
+    doReturn(true).when(sharedRefDb).compareAndPut(A_TEST_PROJECT_NAME, oldRef, newRef);
+    doReturn(Result.NEW).when(refUpdate).update();
+
+    MultiSiteRefUpdate multiSiteRefUpdate =
+        new MultiSiteRefUpdate(sharedRefDb, A_TEST_PROJECT_NAME, refUpdate);
+
+    assertThat(multiSiteRefUpdate.update()).isEqualTo(Result.NEW);
+  }
+
+  @Test(expected = IOException.class)
+  public void newUpdateShouldValidateAndFailWithIOException() throws IOException {
+    setMockRequiredReturnValues();
+
+    // When compareAndPut fails
+    doReturn(false).when(sharedRefDb).compareAndPut(A_TEST_PROJECT_NAME, oldRef, newRef);
+
+    MultiSiteRefUpdate multiSiteRefUpdate =
+        new MultiSiteRefUpdate(sharedRefDb, A_TEST_PROJECT_NAME, refUpdate);
+    multiSiteRefUpdate.update();
+  }
+
+  @Test
+  public void deleteShouldValidateAndSucceed() throws IOException {
+    setMockRequiredReturnValues();
+
+    // When compareAndPut succeeds
+    doReturn(true).when(sharedRefDb).compareAndRemove(A_TEST_PROJECT_NAME, oldRef);
+    doReturn(Result.FORCED).when(refUpdate).delete();
+
+    MultiSiteRefUpdate multiSiteRefUpdate =
+        new MultiSiteRefUpdate(sharedRefDb, A_TEST_PROJECT_NAME, refUpdate);
+
+    assertThat(multiSiteRefUpdate.delete()).isEqualTo(Result.FORCED);
+  }
+
+  @Test(expected = IOException.class)
+  public void deleteShouldValidateAndFailWithIOException() throws IOException {
+    setMockRequiredReturnValues();
+
+    // When compareAndPut fails
+    doReturn(false).when(sharedRefDb).compareAndRemove(A_TEST_PROJECT_NAME, oldRef);
+
+    MultiSiteRefUpdate multiSiteRefUpdate =
+        new MultiSiteRefUpdate(sharedRefDb, A_TEST_PROJECT_NAME, refUpdate);
+    multiSiteRefUpdate.delete();
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRepositoryTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRepositoryTest.java
new file mode 100644
index 0000000..4013344
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteRepositoryTest.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed 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.
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.zookeeper.RefFixture;
+import java.io.IOException;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class MultiSiteRepositoryTest implements RefFixture {
+
+  @Mock MultiSiteRefDatabase.Factory multiSiteRefDbFactory;
+  @Mock MultiSiteRefDatabase multiSiteRefDb;
+  @Mock RefDatabase genericRefDb;
+
+  @Mock MultiSiteRefUpdate multiSiteRefUpdate;
+
+  @Mock Repository repository;
+
+  private final String PROJECT_NAME = "ProjectName";
+  private final String REFS_HEADS_MASTER = "refs/heads/master";
+
+  @Override
+  public String testBranch() {
+    return null;
+  }
+
+  private void setMockitoCommon() {
+    doReturn(genericRefDb).when(repository).getRefDatabase();
+    doReturn(multiSiteRefDb).when(multiSiteRefDbFactory).create(PROJECT_NAME, genericRefDb);
+  }
+
+  @Test
+  public void shouldInvokeMultiSiteRefDbFactoryCreate() {
+    setMockitoCommon();
+    MultiSiteRepository multiSiteRepository =
+        new MultiSiteRepository(multiSiteRefDbFactory, PROJECT_NAME, repository);
+
+    multiSiteRepository.getRefDatabase();
+    verify(multiSiteRefDbFactory).create(PROJECT_NAME, genericRefDb);
+  }
+
+  @Test
+  public void shouldInvokeNewUpdateInMultiSiteRefDatabase() throws IOException {
+    setMockitoCommon();
+    MultiSiteRepository multiSiteRepository =
+        new MultiSiteRepository(multiSiteRefDbFactory, PROJECT_NAME, repository);
+    multiSiteRepository.getRefDatabase().newUpdate(REFS_HEADS_MASTER, false);
+
+    verify(multiSiteRefDb).newUpdate(REFS_HEADS_MASTER, false);
+  }
+
+  @Test
+  public void shouldInvokeUpdateInMultiSiteRefUpdate() throws IOException {
+    setMockitoCommon();
+    doReturn(Result.NEW).when(multiSiteRefUpdate).update();
+    doReturn(multiSiteRefUpdate).when(multiSiteRefDb).newUpdate(REFS_HEADS_MASTER, false);
+
+    MultiSiteRepository multiSiteRepository =
+        new MultiSiteRepository(multiSiteRefDbFactory, PROJECT_NAME, repository);
+
+    Result updateResult =
+        multiSiteRepository.getRefDatabase().newUpdate(REFS_HEADS_MASTER, false).update();
+
+    verify(multiSiteRefUpdate).update();
+    assertThat(updateResult).isEqualTo(Result.NEW);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationIT.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationIT.java
deleted file mode 100644
index 0c6833d..0000000
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationIT.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation;
-
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
-import com.google.gerrit.acceptance.LogThreshold;
-import com.google.gerrit.acceptance.NoHttpd;
-import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.TestPlugin;
-import com.google.inject.AbstractModule;
-import org.junit.Test;
-
-@NoHttpd
-@LogThreshold(level = "INFO")
-@TestPlugin(
-    name = "multi-site",
-    sysModule = "com.googlesource.gerrit.plugins.multisite.validation.ValidationIT$Module")
-public class ValidationIT extends LightweightPluginDaemonTest {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  public static class Module extends AbstractModule {
-    @Override
-    protected void configure() {
-      install(new ValidationModule());
-    }
-  }
-
-  @Test
-  public void inSyncChangeValidatorShouldAcceptNewChange() throws Exception {
-    final PushOneCommit.Result change = createChange("refs/for/master");
-
-    change.assertOkStatus();
-  }
-}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/RefSharedDatabaseTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/RefSharedDatabaseTest.java
new file mode 100644
index 0000000..e0f0a9f
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/RefSharedDatabaseTest.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed 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.
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.zookeeper.RefFixture;
+import java.io.IOException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Ref.Storage;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+public class RefSharedDatabaseTest implements RefFixture {
+  @Rule public TestName nameRule = new TestName();
+
+  @Override
+  public String testBranch() {
+    return "branch_" + nameRule.getMethodName();
+  }
+
+  @Test
+  public void shouldCreateANewRef() {
+
+    ObjectId objectId = AN_OBJECT_ID_1;
+    String refName = aBranchRef();
+
+    Ref aNewRef =
+        new SharedRefDatabase() {
+
+          @Override
+          public boolean compareAndRemove(String project, Ref oldRef) throws IOException {
+            return false;
+          }
+
+          @Override
+          public boolean compareAndPut(String project, Ref oldRef, Ref newRef) throws IOException {
+            return false;
+          }
+        }.newRef(refName, objectId);
+
+    assertThat(aNewRef.getName()).isEqualTo(refName);
+    assertThat(aNewRef.getObjectId()).isEqualTo(objectId);
+    assertThat(aNewRef.getStorage()).isEqualTo(Storage.NETWORK);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/RefFixture.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/RefFixture.java
new file mode 100644
index 0000000..6e64850
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/RefFixture.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed 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.
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.zookeeper;
+
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gerrit.reviewdb.client.RefNames;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Ignore;
+
+@Ignore
+public interface RefFixture {
+
+  static final String ALLOWED_CHARS = "abcdefghilmnopqrstuvz";
+  static final String ALLOWED_DIGITS = "1234567890";
+  static final String ALLOWED_NAME_CHARS =
+      ALLOWED_CHARS + ALLOWED_CHARS.toUpperCase() + ALLOWED_DIGITS;
+  static final String A_TEST_PROJECT_NAME = "A_TEST_PROJECT_NAME";
+  static final NameKey A_TEST_PROJECT_NAME_KEY = new NameKey(A_TEST_PROJECT_NAME);
+  static final ObjectId AN_OBJECT_ID_1 = new ObjectId(1, 2, 3, 4, 5);
+  static final ObjectId AN_OBJECT_ID_2 = new ObjectId(1, 2, 3, 4, 6);
+  static final ObjectId AN_OBJECT_ID_3 = new ObjectId(1, 2, 3, 4, 7);
+  static final String A_TEST_REF_NAME = "refs/heads/master";
+  static final String A_REF_NAME_OF_A_PATCHSET = "refs/changes/01/1/1";
+
+  default String aBranchRef() {
+    return RefNames.REFS_HEADS + testBranch();
+  }
+
+  String testBranch();
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkSharedRefDatabaseTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkSharedRefDatabaseTest.java
new file mode 100644
index 0000000..c3f3ace
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZkSharedRefDatabaseTest.java
@@ -0,0 +1,215 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed 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.
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.zookeeper;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefDatabase;
+import org.apache.curator.retry.RetryNTimes;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+public class ZkSharedRefDatabaseTest implements RefFixture {
+  @Rule public TestName nameRule = new TestName();
+
+  ZookeeperTestContainerSupport zookeeperContainer;
+  ZkSharedRefDatabase zkSharedRefDatabase;
+
+  @Before
+  public void setup() {
+    zookeeperContainer = new ZookeeperTestContainerSupport(false);
+    zkSharedRefDatabase =
+        new ZkSharedRefDatabase(zookeeperContainer.getCurator(), new RetryNTimes(5, 30));
+  }
+
+  @After
+  public void cleanup() {
+    zookeeperContainer.cleanup();
+  }
+
+  @Test
+  public void shouldCompareAndCreateSuccessfully() throws Exception {
+    Ref ref = refOf(AN_OBJECT_ID_1);
+
+    assertThat(zkSharedRefDatabase.compareAndCreate(A_TEST_PROJECT_NAME, ref)).isTrue();
+
+    assertThat(zookeeperContainer.readRefValueFromZk(A_TEST_PROJECT_NAME, ref))
+        .isEqualTo(ref.getObjectId());
+  }
+
+  @Test
+  public void shouldCompareAndPutSuccessfully() throws Exception {
+    Ref oldRef = refOf(AN_OBJECT_ID_1);
+    Ref newRef = refOf(AN_OBJECT_ID_2);
+    String projectName = A_TEST_PROJECT_NAME;
+
+    zookeeperContainer.createRefInZk(projectName, oldRef);
+
+    assertThat(zkSharedRefDatabase.compareAndPut(projectName, oldRef, newRef)).isTrue();
+  }
+
+  @Test
+  public void shouldCompareAndPutWithNullOldRefSuccessfully() throws Exception {
+    Ref oldRef = refOf(null);
+    Ref newRef = refOf(AN_OBJECT_ID_2);
+    String projectName = A_TEST_PROJECT_NAME;
+
+    zookeeperContainer.createRefInZk(projectName, oldRef);
+
+    assertThat(zkSharedRefDatabase.compareAndPut(projectName, oldRef, newRef)).isTrue();
+  }
+
+  @Test
+  public void compareAndPutShouldFailIfTheObjectionHasNotTheExpectedValue() throws Exception {
+    String projectName = A_TEST_PROJECT_NAME;
+
+    Ref oldRef = refOf(AN_OBJECT_ID_1);
+    Ref expectedRef = refOf(AN_OBJECT_ID_2);
+
+    zookeeperContainer.createRefInZk(projectName, oldRef);
+
+    assertThat(zkSharedRefDatabase.compareAndPut(projectName, expectedRef, refOf(AN_OBJECT_ID_3)))
+        .isFalse();
+  }
+
+  @Test
+  public void shouldCompareAndRemoveSuccessfully() throws Exception {
+    Ref oldRef = refOf(AN_OBJECT_ID_1);
+    String projectName = A_TEST_PROJECT_NAME;
+
+    zookeeperContainer.createRefInZk(projectName, oldRef);
+
+    assertThat(zkSharedRefDatabase.compareAndRemove(projectName, oldRef)).isTrue();
+  }
+
+  @Test
+  public void shouldReplaceTheRefWithATombstoneAfterCompareAndPutRemove() throws Exception {
+    Ref oldRef = refOf(AN_OBJECT_ID_1);
+
+    zookeeperContainer.createRefInZk(A_TEST_PROJECT_NAME, oldRef);
+
+    assertThat(zkSharedRefDatabase.compareAndRemove(A_TEST_PROJECT_NAME, oldRef)).isTrue();
+
+    assertThat(zookeeperContainer.readRefValueFromZk(A_TEST_PROJECT_NAME, oldRef))
+        .isEqualTo(ObjectId.zeroId());
+  }
+
+  @Test
+  public void shouldNotCompareAndPutSuccessfullyAfterACompareAndRemove() throws Exception {
+    Ref oldRef = refOf(AN_OBJECT_ID_1);
+    String projectName = A_TEST_PROJECT_NAME;
+
+    zookeeperContainer.createRefInZk(projectName, oldRef);
+
+    zkSharedRefDatabase.compareAndRemove(projectName, oldRef);
+    assertThat(zkSharedRefDatabase.compareAndPut(projectName, oldRef, refOf(AN_OBJECT_ID_2)))
+        .isFalse();
+  }
+
+  private Ref refOf(ObjectId objectId) {
+    return zkSharedRefDatabase.newRef(aBranchRef(), objectId);
+  }
+
+  @Test
+  public void immutableChangeShouldReturnTrue() throws Exception {
+    Ref changeRef = zkSharedRefDatabase.newRef("refs/changes/01/1/1", AN_OBJECT_ID_1);
+
+    boolean shouldReturnTrue =
+        zkSharedRefDatabase.compareAndPut(
+            A_TEST_PROJECT_NAME, SharedRefDatabase.NULL_REF, changeRef);
+
+    assertThat(shouldReturnTrue).isTrue();
+  }
+
+  @Test(expected = Exception.class)
+  public void immutableChangeShouldNotBeStored() throws Exception {
+    Ref changeRef = zkSharedRefDatabase.newRef(A_REF_NAME_OF_A_PATCHSET, AN_OBJECT_ID_1);
+    zkSharedRefDatabase.compareAndPut(A_TEST_PROJECT_NAME, SharedRefDatabase.NULL_REF, changeRef);
+    zookeeperContainer.readRefValueFromZk(A_TEST_PROJECT_NAME, changeRef);
+  }
+
+  @Test
+  public void anImmutableChangeShouldBeIgnored() {
+    Ref immutableChangeRef = zkSharedRefDatabase.newRef(A_REF_NAME_OF_A_PATCHSET, AN_OBJECT_ID_1);
+    assertThat(zkSharedRefDatabase.ignoreRefInSharedDb(immutableChangeRef.getName())).isTrue();
+  }
+
+  @Test
+  public void aChangeMetaShouldNotBeIgnored() {
+    Ref immutableChangeRef = zkSharedRefDatabase.newRef("refs/changes/01/1/meta", AN_OBJECT_ID_1);
+    assertThat(zkSharedRefDatabase.ignoreRefInSharedDb(immutableChangeRef.getName())).isFalse();
+  }
+
+  @Test
+  public void aDraftCommentsShouldBeIgnored() {
+    Ref immutableChangeRef =
+        zkSharedRefDatabase.newRef("refs/draft-comments/01/1/1000000", AN_OBJECT_ID_1);
+    assertThat(zkSharedRefDatabase.ignoreRefInSharedDb(immutableChangeRef.getName())).isTrue();
+  }
+
+  @Test
+  public void regularRefHeadsMasterShouldNotBeIgnored() {
+    Ref immutableChangeRef = zkSharedRefDatabase.newRef("refs/heads/master", AN_OBJECT_ID_1);
+    assertThat(zkSharedRefDatabase.ignoreRefInSharedDb(immutableChangeRef.getName())).isFalse();
+  }
+
+  @Test
+  public void regularCommitShouldNotBeIgnored() {
+    Ref immutableChangeRef = zkSharedRefDatabase.newRef("refs/heads/stable-2.16", AN_OBJECT_ID_1);
+    assertThat(zkSharedRefDatabase.ignoreRefInSharedDb(immutableChangeRef.getName())).isFalse();
+  }
+
+  @Test
+  public void compareAndPutShouldAlwaysIngoreAlwaysDraftCommentsEvenOutOfOrder() throws Exception {
+    // Test to reproduce a production bug where ignored refs were persisted in ZK because
+    // newRef == NULL
+    Ref existingRef =
+        zkSharedRefDatabase.newRef("refs/draft-comments/56/450756/1013728", AN_OBJECT_ID_1);
+    Ref oldRefToIgnore =
+        zkSharedRefDatabase.newRef("refs/draft-comments/56/450756/1013728", AN_OBJECT_ID_2);
+    Ref nullRef = SharedRefDatabase.NULL_REF;
+    String projectName = A_TEST_PROJECT_NAME;
+
+    // This ref should be ignored even if newRef is null
+    assertThat(zkSharedRefDatabase.compareAndPut(A_TEST_PROJECT_NAME, existingRef, nullRef))
+        .isTrue();
+
+    // This ignored ref should also be ignored
+    assertThat(zkSharedRefDatabase.compareAndPut(projectName, oldRefToIgnore, nullRef)).isTrue();
+  }
+
+  @Override
+  public String testBranch() {
+    return "branch_" + nameRule.getMethodName();
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZookeeperTestContainerSupport.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZookeeperTestContainerSupport.java
new file mode 100644
index 0000000..e7e7838
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/zookeeper/ZookeeperTestContainerSupport.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed 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.
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.zookeeper;
+
+import static com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefDatabase.NULL_REF;
+import static com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.zookeeper.ZkSharedRefDatabase.pathFor;
+import static com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.zookeeper.ZkSharedRefDatabase.writeObjectId;
+
+import com.googlesource.gerrit.plugins.multisite.Configuration;
+import org.apache.curator.framework.CuratorFramework;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.junit.Ignore;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+
+@Ignore
+public class ZookeeperTestContainerSupport {
+
+  static class ZookeeperContainer extends GenericContainer<ZookeeperContainer> {
+    public static String ZOOKEEPER_VERSION = "3.4.13";
+
+    public ZookeeperContainer() {
+      super("zookeeper:" + ZOOKEEPER_VERSION);
+    }
+  }
+
+  private ZookeeperContainer container;
+  private Configuration configuration;
+  private CuratorFramework curator;
+
+  public CuratorFramework getCurator() {
+    return curator;
+  }
+
+  public ZookeeperContainer getContainer() {
+    return container;
+  }
+
+  public Configuration getConfig() {
+    return configuration;
+  }
+
+  @SuppressWarnings("resource")
+  public ZookeeperTestContainerSupport(boolean migrationMode) {
+    container = new ZookeeperContainer().withExposedPorts(2181).waitingFor(Wait.forListeningPort());
+    container.start();
+    Integer zkHostPort = container.getMappedPort(2181);
+    Config splitBrainconfig = new Config();
+    String connectString = container.getContainerIpAddress() + ":" + zkHostPort;
+    splitBrainconfig.setBoolean("ref-database", null, "enabled", true);
+    splitBrainconfig.setString("ref-database", "zookeeper", "connectString", connectString);
+    splitBrainconfig.setString(
+        "ref-database",
+        Configuration.ZookeeperConfig.SUBSECTION,
+        Configuration.ZookeeperConfig.KEY_CONNECT_STRING,
+        connectString);
+    splitBrainconfig.setBoolean(
+        "ref-database",
+        Configuration.ZookeeperConfig.SUBSECTION,
+        Configuration.ZookeeperConfig.KEY_MIGRATE,
+        migrationMode);
+
+    configuration = new Configuration(splitBrainconfig, new Config());
+    this.curator = configuration.getZookeeperConfig().buildCurator();
+  }
+
+  public void cleanup() {
+    this.curator.delete();
+    this.container.stop();
+  }
+
+  public ObjectId readRefValueFromZk(String projectName, Ref ref) throws Exception {
+    final byte[] bytes = curator.getData().forPath(pathFor(projectName, NULL_REF, ref));
+    return ZkSharedRefDatabase.readObjectId(bytes);
+  }
+
+  public void createRefInZk(String projectName, Ref ref) throws Exception {
+    curator
+        .create()
+        .creatingParentContainersIfNeeded()
+        .forPath(pathFor(projectName, NULL_REF, ref), writeObjectId(ref.getObjectId()));
+  }
+}