CASSANDRASC-77: Upgrade vertx to version 4.4.6 to bring hot reloading and traffic shaping options

Vertx 4.4.6 brings two features that we integrate into Sidecar.

1. Hot reloading of SSL certificates. This allows a running cluster to reload
   certificates without having to restart the service.

2. Traffic shaping options. This allows to introduce protections for the
   service. It allows configuring ingress/egress limits.

Additionally, this patch introduces the SidecarServerEvents messaging. It
leverages vertx's EventBus to publish and consume messages when server
starts, server stops, on CQL connection ready, or CQL disconnection,
and when all CQL connections are ready.

Patch by Francisco Guerrero; Reviewed by Dinesh Joshi, Yifan Cai for CASSANDRASC-77
diff --git a/CHANGES.txt b/CHANGES.txt
index 255f614..62b2f77 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
 1.0.0
 -----
+ * Upgrade vertx to version 4.4.6 to bring hot reloading and traffic shaping options (CASSANDRASC-77)
  * Fix unable to stream secondary index files (CASSANDRASC-74)
  * Updates token-ranges endpoint to return additional instance metadata (CASSANDRASC-73)
  * Shade Jackson completely to prevent incompatibility issues (CASSANDRASC-75)
diff --git a/adapters/base/src/main/java/org/apache/cassandra/sidecar/adapters/base/CassandraStorageOperations.java b/adapters/base/src/main/java/org/apache/cassandra/sidecar/adapters/base/CassandraStorageOperations.java
index efe0fa1..58d6040 100644
--- a/adapters/base/src/main/java/org/apache/cassandra/sidecar/adapters/base/CassandraStorageOperations.java
+++ b/adapters/base/src/main/java/org/apache/cassandra/sidecar/adapters/base/CassandraStorageOperations.java
@@ -51,14 +51,31 @@
     /**
      * Creates a new instance with the provided {@link JmxClient} and {@link DnsResolver}
      *
-     * @param jmxClient the JMX client used to communicate with the Cassandra instance
+     * @param jmxClient   the JMX client used to communicate with the Cassandra instance
      * @param dnsResolver the DNS resolver used to lookup replicas
      */
     public CassandraStorageOperations(JmxClient jmxClient, DnsResolver dnsResolver)
     {
+        this(jmxClient,
+             new RingProvider(jmxClient, dnsResolver),
+             new TokenRangeReplicaProvider(jmxClient, dnsResolver));
+    }
+
+    /**
+     * Creates a new instances with the provided {@link JmxClient}, {@link RingProvider}, and
+     * {@link TokenRangeReplicaProvider}. This constructor is exposed for extensibility.
+     *
+     * @param jmxClient                 the JMX client used to communicate with the Cassandra instance
+     * @param ringProvider              the ring provider instance
+     * @param tokenRangeReplicaProvider the token range replica provider
+     */
+    public CassandraStorageOperations(JmxClient jmxClient,
+                                      RingProvider ringProvider,
+                                      TokenRangeReplicaProvider tokenRangeReplicaProvider)
+    {
         this.jmxClient = jmxClient;
-        this.ringProvider = new RingProvider(jmxClient, dnsResolver);
-        this.tokenRangeReplicaProvider =  new TokenRangeReplicaProvider(jmxClient, dnsResolver);
+        this.ringProvider = ringProvider;
+        this.tokenRangeReplicaProvider = tokenRangeReplicaProvider;
     }
 
     /**
diff --git a/build.gradle b/build.gradle
index f0ca960..3303d6f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -127,6 +127,7 @@
 
 run {
     confFile = "file:" + File.separator + File.separator + "$projectDir/conf/sidecar.yaml"
+    println "Sidecar configuration file $confFile"
     jvmArgs = ["-Dsidecar.logdir=./logs",
                "-Dsidecar.config=" + confFile,
                "-Dlogback.configurationFile=./conf/logback.xml",
diff --git a/client/src/main/java/org/apache/cassandra/sidecar/client/retry/BasicRetryPolicy.java b/client/src/main/java/org/apache/cassandra/sidecar/client/retry/BasicRetryPolicy.java
index fc4c067..75b167c 100644
--- a/client/src/main/java/org/apache/cassandra/sidecar/client/retry/BasicRetryPolicy.java
+++ b/client/src/main/java/org/apache/cassandra/sidecar/client/retry/BasicRetryPolicy.java
@@ -71,7 +71,6 @@
         this.retryDelayMillis = retryDelayMillis;
     }
 
-
     /**
      * {@inheritDoc}
      */
diff --git a/common/src/main/java/org/apache/cassandra/sidecar/common/JmxClient.java b/common/src/main/java/org/apache/cassandra/sidecar/common/JmxClient.java
index 91196ed..2bb86d8 100644
--- a/common/src/main/java/org/apache/cassandra/sidecar/common/JmxClient.java
+++ b/common/src/main/java/org/apache/cassandra/sidecar/common/JmxClient.java
@@ -42,12 +42,11 @@
 import javax.rmi.ssl.SslRMIClientSocketFactory;
 
 import com.google.common.util.concurrent.Uninterruptibles;
-
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.sidecar.common.exceptions.JmxAuthenticationException;
-import org.jetbrains.annotations.VisibleForTesting;
+import org.apache.cassandra.sidecar.common.utils.Preconditions;
 
 /**
  * A simple wrapper around a JMX connection that makes it easier to get proxy instances.
@@ -65,94 +64,34 @@
     private final Supplier<String> roleSupplier;
     private final Supplier<String> passwordSupplier;
     private final BooleanSupplier enableSslSupplier;
-    private final int jmxConnectionMaxRetries;
-    private final long jmxConnectionRetryDelayMillis;
+    private final int connectionMaxRetries;
+    private final long connectionRetryDelayMillis;
+
 
     /**
-     * Creates a new client with the provided {@code host} and {@code port}.
+     * Creates a new JMX client with {@link Builder} options.
      *
-     * @param host the host of the JMX service
-     * @param port the port of the JMX service
+     * @param builder the builder options
      */
-    public JmxClient(String host, int port)
+    private JmxClient(Builder builder)
     {
-        this(host, port, null, null, false);
-    }
-
-    /**
-     * Creates a new client with the provided parameters
-     *
-     * @param host      the host of the JMX service
-     * @param port      the port of the JMX service
-     * @param role      the JMX role used for authentication
-     * @param password  the JMX role password used for authentication
-     * @param enableSSl true if SSL is enabled for JMX, false otherwise
-     */
-    public JmxClient(String host, int port, String role, String password, boolean enableSSl)
-    {
-        this(buildJmxServiceURL(host, port), () -> role, () -> password, () -> enableSSl);
-    }
-
-    /**
-     * Creates a new client with the provided parameters
-     *
-     * @param host                       the host of the JMX service
-     * @param port                       the port of the JMX service
-     * @param role                       the JMX role used for authentication
-     * @param password                   the JMX role password used for authentication
-     * @param enableSSl                  true if SSL is enabled for JMX, false otherwise
-     * @param connectionMaxRetries       the maximum number of connection retries before failing to connect
-     * @param connectionRetryDelayMillis the number of milliseconds to delay between connection retries
-     */
-    public JmxClient(String host, int port, String role, String password,
-                     boolean enableSSl, int connectionMaxRetries, long connectionRetryDelayMillis)
-    {
-        this(buildJmxServiceURL(host, port), () -> role, () -> password, () -> enableSSl,
-             connectionMaxRetries, connectionRetryDelayMillis);
-    }
-
-    @VisibleForTesting
-    JmxClient(JMXServiceURL jmxServiceURL)
-    {
-        this(jmxServiceURL, () -> null, () -> null, () -> false);
-    }
-
-    @VisibleForTesting
-    JmxClient(JMXServiceURL jmxServiceURL, String role, String password)
-    {
-        this(jmxServiceURL, () -> role, () -> password, () -> false);
-    }
-
-    public JmxClient(String host,
-                     int port,
-                     Supplier<String> roleSupplier,
-                     Supplier<String> passwordSupplier,
-                     BooleanSupplier enableSslSupplier)
-    {
-        this(buildJmxServiceURL(host, port), roleSupplier, passwordSupplier, enableSslSupplier);
-    }
-
-    public JmxClient(JMXServiceURL jmxServiceURL,
-                     Supplier<String> roleSupplier,
-                     Supplier<String> passwordSupplier,
-                     BooleanSupplier enableSslSupplier)
-    {
-        this(jmxServiceURL, roleSupplier, passwordSupplier, enableSslSupplier, 20, 1000);
-    }
-
-    public JmxClient(JMXServiceURL jmxServiceURL,
-                     Supplier<String> roleSupplier,
-                     Supplier<String> passwordSupplier,
-                     BooleanSupplier enableSslSupplier,
-                     int jmxConnectionMaxRetries,
-                     long jmxConnectionRetryDelayMillis)
-    {
-        this.jmxServiceURL = Objects.requireNonNull(jmxServiceURL, "jmxServiceURL is required");
-        this.roleSupplier = Objects.requireNonNull(roleSupplier, "roleSupplier is required");
-        this.passwordSupplier = Objects.requireNonNull(passwordSupplier, "passwordSupplier is required");
-        this.enableSslSupplier = Objects.requireNonNull(enableSslSupplier, "enableSslSupplier is required");
-        this.jmxConnectionMaxRetries = jmxConnectionMaxRetries;
-        this.jmxConnectionRetryDelayMillis = jmxConnectionRetryDelayMillis;
+        if (builder.jmxServiceURL != null)
+        {
+            jmxServiceURL = builder.jmxServiceURL;
+        }
+        else
+        {
+            jmxServiceURL = buildJmxServiceURL(Objects.requireNonNull(builder.host, "host is required"),
+                                               builder.port);
+        }
+        Objects.requireNonNull(jmxServiceURL, "jmxServiceUrl is required");
+        roleSupplier = Objects.requireNonNull(builder.roleSupplier, "roleSupplier is required");
+        passwordSupplier = Objects.requireNonNull(builder.passwordSupplier, "passwordSupplier is required");
+        enableSslSupplier = Objects.requireNonNull(builder.enableSslSupplier, "enableSslSupplier is required");
+        Preconditions.checkArgument(builder.connectionMaxRetries > 0,
+                                    "connectionMaxRetries must be a positive integer");
+        connectionMaxRetries = builder.connectionMaxRetries;
+        connectionRetryDelayMillis = builder.connectionRetryDelayMillis;
     }
 
     /**
@@ -198,7 +137,7 @@
     private void connect()
     {
         int attempts = 1;
-        int maxAttempts = jmxConnectionMaxRetries;
+        int maxAttempts = connectionMaxRetries;
         Throwable lastThrown = null;
         while (attempts <= maxAttempts)
         {
@@ -230,7 +169,7 @@
                 {
                     LOGGER.info("Could not connect to JMX on {} after {} attempts. Will retry.",
                                 jmxServiceURL, attempts, t);
-                    Uninterruptibles.sleepUninterruptibly(jmxConnectionRetryDelayMillis, TimeUnit.MILLISECONDS);
+                    Uninterruptibles.sleepUninterruptibly(connectionRetryDelayMillis, TimeUnit.MILLISECONDS);
                 }
                 attempts++;
             }
@@ -354,4 +293,168 @@
         // Use square brackets to surround IPv6 addresses to fix CASSANDRA-7669 and CASSANDRA-17581
         return "[" + host + "]";
     }
+
+    public static Builder builder()
+    {
+        return new Builder();
+    }
+
+    /**
+     * {@code JmxClient} builder static inner class.
+     */
+    public static final class Builder implements DataObjectBuilder<Builder, JmxClient>
+    {
+        private JMXServiceURL jmxServiceURL;
+        private String host;
+        private int port;
+        private Supplier<String> roleSupplier = () -> null;
+        private Supplier<String> passwordSupplier = () -> null;
+        private BooleanSupplier enableSslSupplier = () -> false;
+        private int connectionMaxRetries = 3;
+        private long connectionRetryDelayMillis = 200;
+
+        private Builder()
+        {
+        }
+
+        @Override
+        public Builder self()
+        {
+            return this;
+        }
+
+        /**
+         * Sets the {@code host} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param host the {@code host} to set
+         * @return a reference to this Builder
+         */
+        public Builder host(String host)
+        {
+            return update(b -> b.host = host);
+        }
+
+        /**
+         * Sets the {@code port} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param port the {@code port} to set
+         * @return a reference to this Builder
+         */
+        public Builder port(int port)
+        {
+            return update(b -> b.port = port);
+        }
+
+        /**
+         * Sets the {@code jmxServiceURL} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param jmxServiceURL the {@code jmxServiceURL} to set
+         * @return a reference to this Builder
+         */
+        public Builder jmxServiceURL(JMXServiceURL jmxServiceURL)
+        {
+            return update(b -> b.jmxServiceURL = jmxServiceURL);
+        }
+
+        /**
+         * Sets the {@code roleSupplier} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param roleSupplier the {@code roleSupplier} to set
+         * @return a reference to this Builder
+         */
+        public Builder roleSupplier(Supplier<String> roleSupplier)
+        {
+            return update(b -> b.roleSupplier = Objects.requireNonNull(roleSupplier,
+                                                                       "roleSupplier must be provided"));
+        }
+
+        /**
+         * Sets the {@code roleSupplier} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param role the {@code role} to set
+         * @return a reference to this Builder
+         */
+        public Builder role(String role)
+        {
+            return update(b -> b.roleSupplier = () -> role);
+        }
+
+        /**
+         * Sets the {@code passwordSupplier} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param passwordSupplier the {@code passwordSupplier} to set
+         * @return a reference to this Builder
+         */
+        public Builder passwordSupplier(Supplier<String> passwordSupplier)
+        {
+            return update(b -> b.passwordSupplier = Objects.requireNonNull(passwordSupplier,
+                                                                           "passwordSupplier must be provided"));
+        }
+
+        /**
+         * Sets the {@code passwordSupplier} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param password the {@code password} to set
+         * @return a reference to this Builder
+         */
+        public Builder password(String password)
+        {
+            return update(b -> b.passwordSupplier = () -> password);
+        }
+
+        /**
+         * Sets the {@code enableSslSupplier} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param enableSslSupplier the {@code enableSslSupplier} to set
+         * @return a reference to this Builder
+         */
+        public Builder enableSslSupplier(BooleanSupplier enableSslSupplier)
+        {
+            return update(b -> b.enableSslSupplier = enableSslSupplier);
+        }
+
+        /**
+         * Sets the {@code enableSslSupplier} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param enableSsl the {@code enableSsl} to set
+         * @return a reference to this Builder
+         */
+        public Builder enableSsl(boolean enableSsl)
+        {
+            return update(b -> b.enableSslSupplier = () -> enableSsl);
+        }
+
+        /**
+         * Sets the {@code connectionMaxRetries} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param connectionMaxRetries the {@code connectionMaxRetries} to set
+         * @return a reference to this Builder
+         */
+        public Builder connectionMaxRetries(int connectionMaxRetries)
+        {
+            return update(b -> b.connectionMaxRetries = connectionMaxRetries);
+        }
+
+        /**
+         * Sets the {@code connectionRetryDelayMillis} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param connectionRetryDelayMillis the {@code connectionRetryDelayMillis} to set
+         * @return a reference to this Builder
+         */
+        public Builder connectionRetryDelayMillis(long connectionRetryDelayMillis)
+        {
+            return update(b -> b.connectionRetryDelayMillis = connectionRetryDelayMillis);
+        }
+
+        /**
+         * Returns a {@code JmxClient} built from the parameters previously set.
+         *
+         * @return a {@code JmxClient} built with parameters of this {@code JmxClient.Builder}
+         */
+        @Override
+        public JmxClient build()
+        {
+            return new JmxClient(this);
+        }
+    }
 }
diff --git a/common/src/test/java/org/apache/cassandra/sidecar/common/JmxClientTest.java b/common/src/test/java/org/apache/cassandra/sidecar/common/JmxClientTest.java
index 6e37100..0744bbc 100644
--- a/common/src/test/java/org/apache/cassandra/sidecar/common/JmxClientTest.java
+++ b/common/src/test/java/org/apache/cassandra/sidecar/common/JmxClientTest.java
@@ -121,7 +121,11 @@
     public void testCanCallMethodWithoutEntireInterface() throws IOException
     {
         List<String> result;
-        try (JmxClient client = new JmxClient(serviceURL, "controlRole", "password"))
+        try (JmxClient client = JmxClient.builder()
+                                         .jmxServiceURL(serviceURL)
+                                         .role("controlRole")
+                                         .password("password")
+                                         .build())
         {
             result = client.proxy(Import.class, objectName)
                            .importNewSSTables(Sets.newHashSet("foo", "bar"), true,
@@ -137,7 +141,11 @@
         importMBean.shouldSucceed = false;
         HashSet<String> srcPaths;
         List<String> failedDirs;
-        try (JmxClient client = new JmxClient(serviceURL, "controlRole", "password"))
+        try (JmxClient client = JmxClient.builder()
+                                         .jmxServiceURL(serviceURL)
+                                         .role("controlRole")
+                                         .password("password")
+                                         .build())
         {
             srcPaths = Sets.newHashSet("foo", "bar");
             failedDirs = client.proxy(Import.class, objectName)
@@ -152,7 +160,7 @@
     @Test
     public void testCallWithoutCredentialsFails() throws IOException
     {
-        try (JmxClient client = new JmxClient(serviceURL))
+        try (JmxClient client = JmxClient.builder().jmxServiceURL(serviceURL).build())
         {
             assertThatExceptionOfType(JmxAuthenticationException.class)
             .isThrownBy(() ->
@@ -175,8 +183,10 @@
         Supplier<String> roleSupplier = () -> {
             throw new IllegalStateException(errorMessage);
         };
-        testSupplierThrows(errorMessage,
-                           new JmxClient(serviceURL, roleSupplier, () -> "password", () -> false));
+        testSupplierThrows(errorMessage, JmxClient.builder()
+                                                  .jmxServiceURL(serviceURL)
+                                                  .roleSupplier(roleSupplier)
+                                                  .build());
     }
 
     @Test
@@ -186,8 +196,11 @@
         Supplier<String> passwordSupplier = () -> {
             throw new IllegalStateException(errorMessage);
         };
-        testSupplierThrows(errorMessage,
-                           new JmxClient(serviceURL, () -> "controlRole", passwordSupplier, () -> false));
+        testSupplierThrows(errorMessage, JmxClient.builder()
+                                                  .jmxServiceURL(serviceURL)
+                                                  .roleSupplier(() -> "controlRole")
+                                                  .passwordSupplier(passwordSupplier)
+                                                  .build());
     }
 
     @Test
@@ -197,8 +210,12 @@
         BooleanSupplier enableSslSupplier = () -> {
             throw new IllegalStateException(errorMessage);
         };
-        testSupplierThrows(errorMessage,
-                           new JmxClient(serviceURL, () -> "controlRole", () -> "password", enableSslSupplier));
+        testSupplierThrows(errorMessage, JmxClient.builder()
+                                                  .jmxServiceURL(serviceURL)
+                                                  .roleSupplier(() -> "controlRole")
+                                                  .passwordSupplier(() -> "password")
+                                                  .enableSslSupplier(enableSslSupplier)
+                                                  .build());
     }
 
     @Test
@@ -214,7 +231,11 @@
             }
             return "password";
         };
-        try (JmxClient client = new JmxClient(serviceURL, () -> "controlRole", passwordSupplier, () -> false))
+        try (JmxClient client = JmxClient.builder()
+                                         .jmxServiceURL(serviceURL)
+                                         .roleSupplier(() -> "controlRole")
+                                         .passwordSupplier(passwordSupplier)
+                                         .build())
         {
             // First attempt fails
             assertThatExceptionOfType(JmxAuthenticationException.class)
@@ -242,7 +263,11 @@
     public void testDisconnectReconnect() throws Exception
     {
         List<String> result;
-        try (JmxClient client = new JmxClient(serviceURL, "controlRole", "password"))
+        try (JmxClient client = JmxClient.builder()
+                                         .jmxServiceURL(serviceURL)
+                                         .role("controlRole")
+                                         .password("password")
+                                         .build())
         {
             assertThat(client.isConnected()).isFalse();
             result = client.proxy(Import.class, objectName)
@@ -268,7 +293,11 @@
     @Test
     public void testLotsOfProxies() throws IOException
     {
-        try (JmxClient client = new JmxClient(serviceURL, "controlRole", "password"))
+        try (JmxClient client = JmxClient.builder()
+                                         .jmxServiceURL(serviceURL)
+                                         .role("controlRole")
+                                         .password("password")
+                                         .build())
         {
             for (int i = 0; i < PROXIES_TO_TEST; i++)
             {
@@ -285,7 +314,12 @@
     @Test
     public void testConstructorWithHostPort() throws IOException
     {
-        try (JmxClient client = new JmxClient("127.0.0.1", port, () -> "controlRole", () -> "password", () -> false))
+        try (JmxClient client = JmxClient.builder()
+                                         .host("127.0.0.1")
+                                         .port(port)
+                                         .roleSupplier(() -> "controlRole")
+                                         .passwordSupplier(() -> "password")
+                                         .build())
         {
             List<String> result = client.proxy(Import.class, objectName)
                                         .importNewSSTables(Sets.newHashSet("foo", "bar"), true,
diff --git a/gradle.properties b/gradle.properties
index 481aa45..a1c59aa 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -18,7 +18,7 @@
 
 version=1.0-SNAPSHOT
 junitVersion=5.9.2
-vertxVersion=4.2.1
+vertxVersion=4.4.6
 guavaVersion=27.0.1-jre
 slf4jVersion=1.7.36
 jacksonVersion=2.14.3
diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml
index 4134346..2439930 100644
--- a/spotbugs-exclude.xml
+++ b/spotbugs-exclude.xml
@@ -43,12 +43,13 @@
             <Class name="org.apache.cassandra.sidecar.concurrent.ExecutorPools$TaskExecutorPool" />
             <Class name="org.apache.cassandra.sidecar.CassandraSidecarDaemon" />
             <Class name="org.apache.cassandra.sidecar.utils.SSTableImporter" />
+            <Class name="org.apache.cassandra.sidecar.tasks.HealthCheckPeriodicTask" />
         </Or>
     </Match>
 
     <!-- Ignore GC call in CassandraTestTemplate as we want to GC after cluster shutdown -->
     <Match>
-        <Class name="org.apache.cassandra.sidecar.testing.CassandraTestTemplate" />
+        <Class name="org.apache.cassandra.sidecar.test.CassandraTestTemplate" />
         <Bug pattern="DM_GC" />
     </Match>
 
diff --git a/src/main/dist/conf/sidecar.yaml b/src/main/dist/conf/sidecar.yaml
index 4542d77..d61a39c 100644
--- a/src/main/dist/conf/sidecar.yaml
+++ b/src/main/dist/conf/sidecar.yaml
@@ -68,10 +68,20 @@
   port: 9043
   request_idle_timeout_millis: 300000 # this field expects integer value
   request_timeout_millis: 300000
+  tcp_keep_alive: false
+  accept_backlog: 1024
+  server_verticle_instances: 1
   throttle:
     stream_requests_per_sec: 5000
     delay_sec: 5
     timeout_sec: 10
+  traffic_shaping:
+    inbound_global_bandwidth_bps: 0               # 0 implies unthrottled, the inbound bandwidth in bytes per second
+    outbound_global_bandwidth_bps: 0              # 0 implies unthrottled, the outbound bandwidth in bytes per second
+    peak_outbound_global_bandwidth_bps: 419430400 # the peak outbound bandwidth in bytes per second. The default is 400 mebibytes per second
+    max_delay_to_wait_millis: 15000               # 15 seconds
+    check_interval_for_stats_millis: 1000         # 1 second
+    inbound_global_file_bandwidth_bps: 0          # 0 implies unthrottled, the inbound bandwidth allocated for incoming files in bytes per second, upper-bounded by inbound_global_bandwidth_bps
   sstable_upload:
     concurrent_upload_limit: 80
     min_free_space_percent: 10
@@ -94,20 +104,26 @@
   jmx:
     max_retries: 3
     retry_delay_millis: 200
+
 #
 # Enable SSL configuration (Disabled by default)
 #
 #  ssl:
 #    enabled: true
+#    use_openssl: true
+#    handshake_timeout_sec: 10
+#    client_auth: NONE # valid options are NONE, REQUEST, REQUIRED
 #    keystore:
 #      path: "path/to/keystore.p12"
 #      password: password
+#      check_interval_sec: 300
 #    truststore:
 #      path: "path/to/truststore.p12"
 #      password: password
 
 
 healthcheck:
+  initial_delay_millis: 0
   poll_freq_millis: 30000
 
 cassandra_input_validation:
diff --git a/src/main/java/com/google/common/util/concurrent/SidecarRateLimiter.java b/src/main/java/com/google/common/util/concurrent/SidecarRateLimiter.java
index 2ee5033..b5abc14 100644
--- a/src/main/java/com/google/common/util/concurrent/SidecarRateLimiter.java
+++ b/src/main/java/com/google/common/util/concurrent/SidecarRateLimiter.java
@@ -18,117 +18,145 @@
 
 package com.google.common.util.concurrent;
 
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 
 /**
  * Wrapper class over guava Rate Limiter, uses SmoothBursty Ratelimiter. This class mainly exists to expose
- * package protected method queryEarliestAvailable of guava RateLimiter
+ * package protected method queryEarliestAvailable of guava RateLimiter.
+ * <p>
+ * In addition to Guava's Rate Limiter functionality, it adds support for disabling rate-limiting.
  */
+@SuppressWarnings("UnstableApiUsage")
 public class SidecarRateLimiter
 {
-    private final RateLimiter rateLimiter;
+    private final AtomicReference<RateLimiter> ref = new AtomicReference<>(null);
 
     private SidecarRateLimiter(final double permitsPerSecond)
     {
-        this.rateLimiter = RateLimiter.create(permitsPerSecond);
+        if (permitsPerSecond > 0)
+        {
+            RateLimiter rateLimiter = RateLimiter.create(permitsPerSecond);
+            ref.set(rateLimiter);
+        }
     }
 
+    /**
+     * Creates a new {@link SidecarRateLimiter} with the configured {@code permitsPerSecond}. When the
+     * {@code permitsPerSecond} is less than or equal to zero, the rate-limiter is disabled (unthrottled).
+     *
+     * @param permitsPerSecond the rate of the returned {@code RateLimiter}, measured in how many
+     *                         permits become available per second
+     * @return the new instance of the rate limiter
+     */
     public static SidecarRateLimiter create(final double permitsPerSecond)
     {
         return new SidecarRateLimiter(permitsPerSecond);
     }
 
-    // Delegated methods
+    // Attention: Hack to expose the package private method queryEarliestAvailable
 
     /**
-     * Returns the earliest time permits will become available
+     * Returns the earliest time permits will become available. Returns 0 if disabled.
+     *
+     * <br><b>Note:</b> this is a hack to expose the package private method
+     * {@link RateLimiter#queryEarliestAvailable(long)}
      *
      * @param nowMicros current time in micros
      * @return earliest time permits will become available
      */
     public long queryEarliestAvailable(final long nowMicros)
     {
-        return this.rateLimiter.queryEarliestAvailable(nowMicros);
+        RateLimiter rateLimiter = ref.get();
+        return rateLimiter != null ? rateLimiter.queryEarliestAvailable(nowMicros) : 0;
     }
 
     /**
-     * Tries to reserve 1 permit, if not available immediately returns false
+     * Acquires a permit if it can be acquired immediately without delay.
      *
-     * @return {@code true} if the permit was acquired, {@code false} otherwise
+     * @return {@code true} if the permit was acquired or rate limiting is disabled, {@code false} otherwise
      */
     public boolean tryAcquire()
     {
-        return this.rateLimiter.tryAcquire();
+        RateLimiter rateLimiter = ref.get();
+        return rateLimiter == null || rateLimiter.tryAcquire();
     }
 
     /**
      * Updates the stable rate of the internal {@code RateLimiter}, that is, the {@code permitsPerSecond}
-     * argument provided in the factory method that constructed the {@code RateLimiter}.
+     * argument provided in the factory method that constructed the {@code RateLimiter}. Setting the rate to any
+     * value less than or equal to {@code 0}, will disable rate limiting.
      *
      * @param permitsPerSecond the new stable rate of this {@code RateLimiter}
      * @throws IllegalArgumentException if {@code permitsPerSecond} is negative or zero
      */
     public void rate(double permitsPerSecond)
     {
-        this.rateLimiter.setRate(permitsPerSecond);
+        RateLimiter rateLimiter = ref.get();
+
+        if (permitsPerSecond > 0.0)
+        {
+            if (rateLimiter == null)
+            {
+                ref.compareAndSet(null, RateLimiter.create(permitsPerSecond));
+            }
+            else
+            {
+                rateLimiter.setRate(permitsPerSecond);
+            }
+        }
+        else
+        {
+            ref.set(null);
+        }
     }
 
-    public void doSetRate(double permitsPerSecond, long nowMicros)
-    {
-        rateLimiter.doSetRate(permitsPerSecond, nowMicros);
-    }
-
+    /**
+     * Returns the stable rate (as {@code permits per seconds}) with which this {@code SidecarRateLimiter} is
+     * configured with. The initial value of this is the same as the {@code permitsPerSecond} argument passed in
+     * the factory method that produced this {@code SidecarRateLimiter}, and it is only updated after invocations
+     * to {@linkplain #rate(double)}. If rate-limiting has been disabled, then returns {@code 0} to indicate that
+     * no rate limiting has been configured.
+     *
+     * @return the stable rate configured in the rate limiter, or {@code 0} when rate limiting is disabled
+     */
     public double rate()
     {
-        return rateLimiter.getRate();
+        RateLimiter rateLimiter = ref.get();
+        return rateLimiter != null ? rateLimiter.getRate() : 0;
     }
 
-    public double doGetRate()
-    {
-        return rateLimiter.doGetRate();
-    }
-
+    /**
+     * Acquires a single permit from this {@code SidecarRateLimiter}, blocking until the request can be
+     * granted. Tells the amount of time slept, if any. When rate-limiting is disabled, it will return {@code 0.0}
+     * to indicate that no amount of time was spent sleeping.
+     *
+     * <p>This method is equivalent to {@code acquire(1)}.
+     *
+     * @return time spent sleeping to enforce rate, in seconds; 0.0 if not rate-limited
+     */
     @CanIgnoreReturnValue
     public double acquire()
     {
-        return rateLimiter.acquire();
+        RateLimiter rateLimiter = ref.get();
+        return rateLimiter != null ? rateLimiter.acquire() : 0;
     }
 
+    /**
+     * Acquires the given number of permits from this {@code RateLimiter}, blocking until the request
+     * can be granted. Tells the amount of time slept, if any. When rate-limiting is disabled, it will return
+     * {@code 0.0} to indicate that no amount of time was spent sleeping. As opposed to the delegating class,
+     * {@code 0} or negative are allowed permit values, and it will result in essentially disabling rate-limiting
+     * and the method will return {@code 0.0} to indicate that no amount of time was spent sleeping.
+     *
+     * @param permits the number of permits to acquire
+     * @return time spent sleeping to enforce rate, in seconds; 0.0 if not rate-limited
+     */
     @CanIgnoreReturnValue
     public double acquire(int permits)
     {
-        return rateLimiter.acquire(permits);
-    }
-
-    public long reserve(int permits)
-    {
-        return rateLimiter.reserve(permits);
-    }
-
-    public boolean tryAcquire(long timeout, TimeUnit unit)
-    {
-        return rateLimiter.tryAcquire(timeout, unit);
-    }
-
-    public boolean tryAcquire(int permits)
-    {
-        return rateLimiter.tryAcquire(permits);
-    }
-
-    public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
-    {
-        return rateLimiter.tryAcquire(permits, timeout, unit);
-    }
-
-    public long reserveAndGetWaitLength(int permits, long nowMicros)
-    {
-        return rateLimiter.reserveAndGetWaitLength(permits, nowMicros);
-    }
-
-    public long reserveEarliestAvailable(int permits, long nowMicros)
-    {
-        return rateLimiter.reserveEarliestAvailable(permits, nowMicros);
+        RateLimiter rateLimiter = ref.get();
+        return rateLimiter != null && permits > 0 ? rateLimiter.acquire(permits) : 0;
     }
 }
diff --git a/src/main/java/org/apache/cassandra/sidecar/CassandraSidecarDaemon.java b/src/main/java/org/apache/cassandra/sidecar/CassandraSidecarDaemon.java
index 9ebf3ac..fd020a2 100644
--- a/src/main/java/org/apache/cassandra/sidecar/CassandraSidecarDaemon.java
+++ b/src/main/java/org/apache/cassandra/sidecar/CassandraSidecarDaemon.java
@@ -18,177 +18,75 @@
 
 package org.apache.cassandra.sidecar;
 
-import java.io.PrintStream;
-import java.util.ArrayList;
-import java.util.List;
+import java.net.URI;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.concurrent.TimeUnit;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.inject.Guice;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import io.vertx.core.CompositeFuture;
-import io.vertx.core.Future;
-import io.vertx.core.Vertx;
-import io.vertx.core.http.HttpServer;
-import org.apache.cassandra.sidecar.cluster.InstancesConfig;
-import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
-import org.apache.cassandra.sidecar.config.ServiceConfiguration;
-import org.apache.cassandra.sidecar.config.SidecarConfiguration;
-import org.apache.cassandra.sidecar.config.SslConfiguration;
-import org.apache.cassandra.sidecar.utils.SslUtils;
+import org.apache.cassandra.sidecar.server.MainModule;
+import org.apache.cassandra.sidecar.server.Server;
 
 /**
  * Main class for initiating the Cassandra sidecar
  * Note: remember to start and stop all delegates of instances
  */
-@Singleton
 public class CassandraSidecarDaemon
 {
-    private static final Logger logger = LoggerFactory.getLogger(CassandraSidecarDaemon.class);
-    private final Vertx vertx;
-    private final HttpServer server;
-    private final SidecarConfiguration config;
-    private final InstancesConfig instancesConfig;
-    private final ExecutorPools executorPools;
-
-    private long healthCheckTimerId;
-
-    @Inject
-    public CassandraSidecarDaemon(Vertx vertx,
-                                  HttpServer server,
-                                  SidecarConfiguration config,
-                                  InstancesConfig instancesConfig,
-                                  ExecutorPools executorPools)
-    {
-        this.vertx = vertx;
-        this.server = server;
-        this.config = config;
-        this.executorPools = executorPools;
-        this.instancesConfig = instancesConfig;
-    }
+    private static final Logger LOGGER = LoggerFactory.getLogger(CassandraSidecarDaemon.class);
 
     public static void main(String[] args)
     {
-        CassandraSidecarDaemon app = Guice.createInjector(new MainModule())
-                                          .getInstance(CassandraSidecarDaemon.class);
+        String yamlConfigurationPath = System.getProperty("sidecar.config", "file://./conf/config.yaml");
 
-        app.start();
-        Runtime.getRuntime().addShutdownHook(new Thread(app::stop));
-    }
-
-    public void start()
-    {
-        banner(System.out);
-        validate();
-
-        ServiceConfiguration service = config.serviceConfiguration();
-
-        logger.info("Starting Cassandra Sidecar on {}:{}", service.host(), service.port());
-        server.listen(service.port(), service.host())
-              .onSuccess(p -> {
-                  // Run a health check after start up
-                  healthCheck();
-                  // Configure the periodic timer to run subsequent health checks configured
-                  // by the config.getHealthCheckFrequencyMillis() interval
-                  updateHealthChecker(config.healthCheckConfiguration().checkIntervalMillis());
-              });
-    }
-
-    public void stop()
-    {
-        logger.info("Stopping Cassandra Sidecar");
-        List<Future> closingFutures = new ArrayList<>();
-        closingFutures.add(server.close());
-        executorPools.internal().cancelTimer(healthCheckTimerId);
-        instancesConfig.instances()
-                       .forEach(instance ->
-                                closingFutures.add(executorPools.internal()
-                                                                .executeBlocking(p -> instance.delegate().close())));
-
+        Path confPath;
         try
         {
-            // Some closing action is executed on the executorPool (which is closed when closing vertx).
-            // Reflecting the dependency below.
+            confPath = Paths.get(new URI(yamlConfigurationPath));
+        }
+        catch (Throwable e)
+        {
+            throw new RuntimeException("Invalid URI: " + yamlConfigurationPath, e);
+        }
 
-            CompositeFuture.all(closingFutures)
-                           .onComplete(v -> vertx.close()).toCompletionStage()
-                           .toCompletableFuture()
-                           .get(10, TimeUnit.SECONDS);
-            logger.info("Cassandra Sidecar stopped successfully");
+        Server app = Guice.createInjector(new MainModule(confPath)).getInstance(Server.class);
+
+        app.start().onSuccess(deploymentId -> Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+            if (close(app))
+            {
+                LOGGER.info("Cassandra Sidecar stopped successfully");
+            }
+        }))).onFailure(throwable -> {
+            LOGGER.error("Failed to start Sidecar", throwable);
+            close(app);
+            System.exit(1);
+        });
+    }
+
+    /**
+     * Closes the server, waits up to 1 minute for the server to shut down.
+     *
+     * @param app the server
+     * @return {@code true} if the server shutdown successfully, {@code false} otherwise
+     */
+    private static boolean close(Server app)
+    {
+        try
+        {
+            app.close()
+               .toCompletionStage()
+               .toCompletableFuture()
+               .get(1, TimeUnit.MINUTES);
+            return true;
         }
         catch (Exception ex)
         {
-            logger.warn("Failed to stop Sidecar in 10 seconds", ex);
+            LOGGER.warn("Failed to stop Sidecar in 1 minute", ex);
         }
-    }
-
-    private void banner(PrintStream out)
-    {
-        out.println(" _____                               _              _____ _     _                     \n" +
-                    "/  __ \\                             | |            /  ___(_)   | |                    \n" +
-                    "| /  \\/ __ _ ___ ___  __ _ _ __   __| |_ __ __ _   \\ `--. _  __| | ___  ___ __ _ _ __ \n" +
-                    "| |    / _` / __/ __|/ _` | '_ \\ / _` | '__/ _` |   `--. \\ |/ _` |/ _ \\/ __/ _` | '__|\n" +
-                    "| \\__/\\ (_| \\__ \\__ \\ (_| | | | | (_| | | | (_| |  /\\__/ / | (_| |  __/ (_| (_| | |   \n" +
-                    " \\____/\\__,_|___/___/\\__,_|_| |_|\\__,_|_|  \\__,_|  \\____/|_|\\__,_|\\___|\\___\\__,_|_|\n" +
-                    "                                                                                      \n" +
-                    "                                                                                      ");
-    }
-
-    private void validate()
-    {
-        SslConfiguration ssl = config.sslConfiguration();
-        if (ssl != null && ssl.enabled())
-        {
-            try
-            {
-                if (!ssl.isKeystoreConfigured())
-                    throw new IllegalArgumentException("keyStorePath and keyStorePassword must be set if ssl enabled");
-
-                SslUtils.validateSslOpts(ssl.keystore().path(), ssl.keystore().password());
-
-                if (ssl.isTruststoreConfigured())
-                    SslUtils.validateSslOpts(ssl.truststore().path(), ssl.truststore().password());
-            }
-            catch (Exception e)
-            {
-                throw new RuntimeException("Invalid keystore parameters for SSL", e);
-            }
-        }
-    }
-
-    /**
-     * Updates the health check frequency to the provided {@code healthCheckFrequencyMillis} value
-     *
-     * @param healthCheckFrequencyMillis the new health check frequency in milliseconds
-     */
-    public void updateHealthChecker(int healthCheckFrequencyMillis)
-    {
-        if (healthCheckTimerId > 0)
-        {
-            // Stop existing timer
-            executorPools.internal().cancelTimer(healthCheckTimerId);
-            logger.info("Stopped health check timer with timerId={}", healthCheckTimerId);
-        }
-        // TODO: when upgrading to latest vertx version, we can set an initial delay, and the periodic delay
-        healthCheckTimerId = executorPools.internal()
-                                          .setPeriodic(healthCheckFrequencyMillis, t -> healthCheck());
-        logger.info("Started health check with frequency={} and timerId={}",
-                    healthCheckFrequencyMillis, healthCheckTimerId);
-    }
-
-    /**
-     * Checks the health of every instance configured in the {@link InstancesConfig}.
-     * The health check is executed in a blocking thread to prevent the event-loop threads from blocking.
-     */
-    private void healthCheck()
-    {
-        instancesConfig.instances()
-                       .forEach(instanceMetadata ->
-                                executorPools.internal()
-                                             .executeBlocking(promise -> instanceMetadata.delegate().healthCheck()));
+        return false;
     }
 }
 
diff --git a/common/src/main/java/org/apache/cassandra/sidecar/common/CassandraAdapterDelegate.java b/src/main/java/org/apache/cassandra/sidecar/cluster/CassandraAdapterDelegate.java
similarity index 63%
rename from common/src/main/java/org/apache/cassandra/sidecar/common/CassandraAdapterDelegate.java
rename to src/main/java/org/apache/cassandra/sidecar/cluster/CassandraAdapterDelegate.java
index 360389f..7466a8d 100644
--- a/common/src/main/java/org/apache/cassandra/sidecar/common/CassandraAdapterDelegate.java
+++ b/src/main/java/org/apache/cassandra/sidecar/cluster/CassandraAdapterDelegate.java
@@ -16,9 +16,10 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.sidecar.common;
+package org.apache.cassandra.sidecar.cluster;
 
 import java.io.IOException;
+import java.util.Objects;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Function;
 
@@ -31,9 +32,23 @@
 import com.datastax.driver.core.Row;
 import com.datastax.driver.core.Session;
 import com.datastax.driver.core.exceptions.NoHostAvailableException;
+import io.vertx.core.Vertx;
+import io.vertx.core.json.JsonObject;
+import org.apache.cassandra.sidecar.common.CQLSessionProvider;
+import org.apache.cassandra.sidecar.common.ClusterMembershipOperations;
+import org.apache.cassandra.sidecar.common.ICassandraAdapter;
+import org.apache.cassandra.sidecar.common.JmxClient;
+import org.apache.cassandra.sidecar.common.NodeSettings;
+import org.apache.cassandra.sidecar.common.StorageOperations;
+import org.apache.cassandra.sidecar.common.TableOperations;
+import org.apache.cassandra.sidecar.utils.CassandraVersionProvider;
+import org.apache.cassandra.sidecar.utils.SimpleCassandraVersion;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import static org.apache.cassandra.sidecar.server.SidecarServerEvents.ON_CASSANDRA_CQL_DISCONNECTED;
+import static org.apache.cassandra.sidecar.server.SidecarServerEvents.ON_CASSANDRA_CQL_READY;
+
 
 /**
  * Since it's possible for the version of Cassandra to change under us, we need this delegate to wrap the functionality
@@ -49,6 +64,8 @@
 {
     private static final Logger LOGGER = LoggerFactory.getLogger(CassandraAdapterDelegate.class);
 
+    private final Vertx vertx;
+    private final int cassandraInstanceId;
     private final String sidecarVersion;
     private final CassandraVersionProvider versionProvider;
     private final CQLSessionProvider cqlSessionProvider;
@@ -59,30 +76,52 @@
     private final AtomicBoolean registered = new AtomicBoolean(false);
     private final AtomicBoolean isHealthCheckActive = new AtomicBoolean(false);
 
-    public CassandraAdapterDelegate(CassandraVersionProvider versionProvider,
-                                    CQLSessionProvider cqlSessionProvider,
+    /**
+     * Constructs a new {@link CassandraAdapterDelegate} for the given {@code cassandraInstance}
+     *
+     * @param vertx               the vertx instance
+     * @param cassandraInstanceId the cassandra instance identifier
+     * @param versionProvider     a Cassandra version provider
+     * @param session             the session to the Cassandra database
+     * @param jmxClient           the JMX client used to communicate with the Cassandra instance
+     * @param sidecarVersion      the version of the Sidecar from the current binary
+     */
+    public CassandraAdapterDelegate(Vertx vertx,
+                                    int cassandraInstanceId,
+                                    CassandraVersionProvider versionProvider,
+                                    CQLSessionProvider session,
                                     JmxClient jmxClient,
                                     String sidecarVersion)
     {
+        this.vertx = Objects.requireNonNull(vertx);
+        this.cassandraInstanceId = cassandraInstanceId;
         this.sidecarVersion = sidecarVersion;
         this.versionProvider = versionProvider;
-        this.cqlSessionProvider = cqlSessionProvider;
+        this.cqlSessionProvider = session;
         this.jmxClient = jmxClient;
     }
 
     private void maybeRegisterHostListener(@NotNull Session session)
     {
-        if (registered.compareAndSet(false, true))
+        if (!registered.get())
         {
-            session.getCluster().register(this);
+            Cluster cluster = session.getCluster();
+            if (!cluster.isClosed() && registered.compareAndSet(false, true))
+            {
+                cluster.register(this);
+            }
         }
     }
 
     private void maybeUnregisterHostListener(@NotNull Session session)
     {
-        if (registered.compareAndSet(true, false))
+        if (registered.get())
         {
-            session.getCluster().unregister(this);
+            Cluster cluster = session.getCluster();
+            if (!cluster.isClosed() && registered.compareAndSet(true, false))
+            {
+                cluster.unregister(this);
+            }
         }
     }
 
@@ -107,7 +146,8 @@
         }
         else
         {
-            LOGGER.debug("Skipping health check because there's an active check at the moment");
+            LOGGER.debug("Skipping health check for cassandraInstanceId={} because there's " +
+                         "an active check at the moment", cassandraInstanceId);
         }
     }
 
@@ -116,8 +156,9 @@
         Session activeSession = cqlSessionProvider.localCql();
         if (activeSession == null)
         {
-            LOGGER.info("No local CQL session is available. Cassandra is down presumably.");
-            nodeSettings = null;
+            LOGGER.info("No local CQL session is available for cassandraInstanceId={}. " +
+                        "Cassandra instance is down presumably.", cassandraInstanceId);
+            markAsDownAndMaybeNotify();
             return;
         }
 
@@ -141,17 +182,19 @@
                 adapter = versionProvider.cassandra(releaseVersion)
                                          .create(cqlSessionProvider, jmxClient);
                 nodeSettings = newNodeSettings;
-                LOGGER.info("Cassandra version change detected (from={} to={}). New adapter loaded={}",
-                            previousVersion, currentVersion, adapter);
+                LOGGER.info("Cassandra version change detected (from={} to={}) for cassandraInstanceId={}. " +
+                            "New adapter loaded={}", previousVersion, currentVersion, cassandraInstanceId, adapter);
+
+                notifyCqlConnection();
             }
             LOGGER.debug("Cassandra version {}", releaseVersion);
         }
         catch (IllegalArgumentException | NoHostAvailableException e)
         {
-            LOGGER.error("Unexpected error connecting to Cassandra instance.", e);
+            LOGGER.error("Unexpected error connecting to Cassandra instance {}", cassandraInstanceId, e);
             // The cassandra node is down.
             // Unregister the host listener and nullify the session in order to get a new object.
-            nodeSettings = null;
+            markAsDownAndMaybeNotify();
             maybeUnregisterHostListener(activeSession);
             cqlSessionProvider.close();
         }
@@ -214,7 +257,7 @@
     @Override
     public void onDown(Host host)
     {
-        nodeSettings = null;
+        markAsDownAndMaybeNotify();
     }
 
     @Override
@@ -240,19 +283,22 @@
 
     public void close()
     {
-        nodeSettings = null;
+        markAsDownAndMaybeNotify();
         Session activeSession = cqlSessionProvider.close();
         if (activeSession != null)
         {
             maybeUnregisterHostListener(activeSession);
         }
-        try
+        if (jmxClient != null)
         {
-            jmxClient.close();
-        }
-        catch (IOException e)
-        {
-            LOGGER.warn("Unable to close JMX client", e);
+            try
+            {
+                jmxClient.close();
+            }
+            catch (IOException e)
+            {
+                LOGGER.warn("Unable to close JMX client", e);
+            }
         }
     }
 
@@ -262,6 +308,27 @@
         return currentVersion;
     }
 
+    protected void notifyCqlConnection()
+    {
+        JsonObject connectMessage = new JsonObject()
+                                    .put("cassandraInstanceId", cassandraInstanceId);
+        vertx.eventBus().publish(ON_CASSANDRA_CQL_READY.address(), connectMessage);
+        LOGGER.info("CQL connected to cassandraInstanceId={}", cassandraInstanceId);
+    }
+
+    protected void markAsDownAndMaybeNotify()
+    {
+        NodeSettings currentNodeSettings = nodeSettings;
+        nodeSettings = null;
+        if (currentNodeSettings != null)
+        {
+            JsonObject disconnectMessage = new JsonObject()
+                                           .put("cassandraInstanceId", cassandraInstanceId);
+            vertx.eventBus().publish(ON_CASSANDRA_CQL_DISCONNECTED.address(), disconnectMessage);
+            LOGGER.info("CQL disconnection from cassandraInstanceId={}", cassandraInstanceId);
+        }
+    }
+
     @Nullable
     private <T> T fromAdapter(Function<ICassandraAdapter, T> getter)
     {
diff --git a/common/src/main/java/org/apache/cassandra/sidecar/cluster/InstancesConfig.java b/src/main/java/org/apache/cassandra/sidecar/cluster/InstancesConfig.java
similarity index 100%
rename from common/src/main/java/org/apache/cassandra/sidecar/cluster/InstancesConfig.java
rename to src/main/java/org/apache/cassandra/sidecar/cluster/InstancesConfig.java
diff --git a/common/src/main/java/org/apache/cassandra/sidecar/cluster/InstancesConfigImpl.java b/src/main/java/org/apache/cassandra/sidecar/cluster/InstancesConfigImpl.java
similarity index 100%
rename from common/src/main/java/org/apache/cassandra/sidecar/cluster/InstancesConfigImpl.java
rename to src/main/java/org/apache/cassandra/sidecar/cluster/InstancesConfigImpl.java
diff --git a/common/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadata.java b/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadata.java
similarity index 95%
rename from common/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadata.java
rename to src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadata.java
index 42eff55..c00dc37 100644
--- a/common/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadata.java
+++ b/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadata.java
@@ -20,7 +20,8 @@
 
 import java.util.List;
 
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
+
 
 /**
  * Metadata of an instance
diff --git a/common/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadataImpl.java b/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadataImpl.java
similarity index 98%
rename from common/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadataImpl.java
rename to src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadataImpl.java
index cb75236..c1a36a7 100644
--- a/common/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadataImpl.java
+++ b/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadataImpl.java
@@ -22,7 +22,7 @@
 import java.util.Collections;
 import java.util.List;
 
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.common.DataObjectBuilder;
 
 /**
diff --git a/src/main/java/org/apache/cassandra/sidecar/concurrent/ExecutorPools.java b/src/main/java/org/apache/cassandra/sidecar/concurrent/ExecutorPools.java
index 02576a2..758d5f2 100644
--- a/src/main/java/org/apache/cassandra/sidecar/concurrent/ExecutorPools.java
+++ b/src/main/java/org/apache/cassandra/sidecar/concurrent/ExecutorPools.java
@@ -18,6 +18,7 @@
 
 package org.apache.cassandra.sidecar.concurrent;
 
+import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
 
 import com.google.inject.Inject;
@@ -118,10 +119,42 @@
          */
         public long setPeriodic(long delay, Handler<Long> handler, boolean ordered)
         {
-            return vertx.setPeriodic(delay,
-                                     id -> workerExecutor.executeBlocking(promise -> {
+            return setPeriodic(delay, delay, handler, ordered);
+        }
+
+        /**
+         * Set a periodic timer to fire every {@code delay} milliseconds with initial delay, at which point
+         * {@code handler} will be called with the id of the timer.
+         *
+         * @param initialDelay the initial delay in milliseconds
+         * @param delay        the delay in milliseconds, after which the timer will fire
+         * @param handler      the handler that will be called with the timer ID when the timer fires
+         * @return the unique ID of the timer
+         */
+        public long setPeriodic(long initialDelay, long delay, Handler<Long> handler)
+        {
+            return setPeriodic(initialDelay, delay, handler, false);
+        }
+
+        /**
+         * Set a periodic timer to fire every {@code delay} milliseconds with initial delay, at which point
+         * {@code handler} will be called with the id of the timer.
+         *
+         * @param initialDelay the initial delay in milliseconds
+         * @param delay        the delay in milliseconds, after which the timer will fire
+         * @param handler      the handler that will be called with the timer ID when the timer fires
+         * @param ordered      if true then executeBlocking is called several times on the same context, the
+         *                     executions for that context will be executed serially, not in parallel. if false
+         *                     then they will be no ordering guarantees
+         * @return the unique ID of the timer
+         */
+        public long setPeriodic(long initialDelay, long delay, Handler<Long> handler, boolean ordered)
+        {
+            return vertx.setPeriodic(initialDelay,
+                                     delay,
+                                     id -> workerExecutor.executeBlocking(() -> {
                                          handler.handle(id);
-                                         promise.complete();
+                                         return id;
                                      }, ordered));
         }
 
@@ -135,7 +168,8 @@
          */
         public long setTimer(long delay, Handler<Long> handler)
         {
-            return vertx.setTimer(delay, id -> workerExecutor.executeBlocking(promise -> handler.handle(id), false));
+            return vertx.setTimer(delay, id ->
+                                         workerExecutor.executeBlocking(promise -> handler.handle(id), false));
         }
 
         /**
@@ -151,7 +185,10 @@
          */
         public long setTimer(long delay, Handler<Long> handler, boolean ordered)
         {
-            return vertx.setTimer(delay, id -> workerExecutor.executeBlocking(promise -> handler.handle(id), ordered));
+            return vertx.setTimer(delay, id -> workerExecutor.executeBlocking(() -> {
+                handler.handle(id);
+                return id;
+            }, ordered));
         }
 
         /**
@@ -181,6 +218,12 @@
         }
 
         @Override
+        public <T> Future<T> executeBlocking(Callable<T> blockingCodeHandler, boolean ordered)
+        {
+            return workerExecutor.executeBlocking(blockingCodeHandler, ordered);
+        }
+
+        @Override
         public <T> void executeBlocking(Handler<Promise<T>> blockingCodeHandler,
                                         Handler<AsyncResult<T>> asyncResultHandler)
         {
diff --git a/src/main/java/org/apache/cassandra/sidecar/config/HealthCheckConfiguration.java b/src/main/java/org/apache/cassandra/sidecar/config/HealthCheckConfiguration.java
index db68807..47509c8 100644
--- a/src/main/java/org/apache/cassandra/sidecar/config/HealthCheckConfiguration.java
+++ b/src/main/java/org/apache/cassandra/sidecar/config/HealthCheckConfiguration.java
@@ -24,6 +24,11 @@
 public interface HealthCheckConfiguration
 {
     /**
+     * @return the initial delay for the first health check, in milliseconds
+     */
+    int initialDelayMillis();
+
+    /**
      * @return the interval, in milliseconds, in which the health checks will be performed
      */
     int checkIntervalMillis();
diff --git a/src/main/java/org/apache/cassandra/sidecar/config/KeyStoreConfiguration.java b/src/main/java/org/apache/cassandra/sidecar/config/KeyStoreConfiguration.java
index 1fd786d..3c74608 100644
--- a/src/main/java/org/apache/cassandra/sidecar/config/KeyStoreConfiguration.java
+++ b/src/main/java/org/apache/cassandra/sidecar/config/KeyStoreConfiguration.java
@@ -23,6 +23,9 @@
  */
 public interface KeyStoreConfiguration
 {
+    String DEFAULT_TYPE = "JKS";
+    int DEFAULT_CHECK_INTERVAL_SECONDS = -1;
+
     /**
      * @return the path to the store
      */
@@ -39,6 +42,22 @@
     String type();
 
     /**
+     * Returns the interval, in seconds, in which the key store will be checked for filesystem changes. Setting
+     * this value to 0 or negative will disable reloading the store.
+     *
+     * @return the interval, in seconds, in which the key store will be checked for changes in the filesystem
+     */
+    int checkIntervalInSeconds();
+
+    /**
+     * @return {@code true} if the key store will be reloaded if a change is detected, {@code false} otherwise
+     */
+    default boolean reloadStore()
+    {
+        return checkIntervalInSeconds() > 0;
+    }
+
+    /**
      * @return {@code true} if both {@link #path()} and {@link #password()} are provided
      */
     default boolean isConfigured()
diff --git a/src/main/java/org/apache/cassandra/sidecar/config/ServiceConfiguration.java b/src/main/java/org/apache/cassandra/sidecar/config/ServiceConfiguration.java
index d108c73..d24d38c 100644
--- a/src/main/java/org/apache/cassandra/sidecar/config/ServiceConfiguration.java
+++ b/src/main/java/org/apache/cassandra/sidecar/config/ServiceConfiguration.java
@@ -18,7 +18,13 @@
 
 package org.apache.cassandra.sidecar.config;
 
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+
+import io.vertx.core.net.SocketAddress;
+import io.vertx.core.net.impl.SocketAddressImpl;
 
 /**
  * Configuration for the Sidecar Service and configuration of the REST endpoints in the service
@@ -34,6 +40,18 @@
     String host();
 
     /**
+     * Returns a list of socket addresses where the Sidecar process will bind and listen for connections. Defaults to
+     * the configured {@link #host()} and {@link #port()}.
+     *
+     * @return a list of socket addresses where Sidecar will listen
+     */
+    default List<SocketAddress> listenSocketAddresses()
+    {
+        return Collections.singletonList(
+        new SocketAddressImpl(port(), Objects.requireNonNull(host(), "host must be provided")));
+    }
+
+    /**
      * @return Sidecar's HTTP REST API port
      */
     int port();
@@ -52,11 +70,26 @@
     long requestTimeoutMillis();
 
     /**
+     * @return {@code true} if TCP keep alive is enabled, {@code false} otherwise
+     */
+    boolean tcpKeepAlive();
+
+    /**
+     * @return the number of connections in the backlog that the incoming queue will hold
+     */
+    int acceptBacklog();
+
+    /**
      * @return the maximum time skew allowed between the server and the client
      */
     int allowableSkewInMinutes();
 
     /**
+     * @return the number of vertx verticle instances that should be deployed
+     */
+    int serverVerticleInstances();
+
+    /**
      * @return the throttling configuration
      */
     ThrottleConfiguration throttleConfiguration();
@@ -96,4 +129,9 @@
      * @return the system-wide JMX configuration settings
      */
     JmxConfiguration jmxConfiguration();
+
+    /**
+     * @return the configuration for the global inbound and outbound traffic shaping options
+     */
+    TrafficShapingConfiguration trafficShapingConfiguration();
 }
diff --git a/src/main/java/org/apache/cassandra/sidecar/config/SslConfiguration.java b/src/main/java/org/apache/cassandra/sidecar/config/SslConfiguration.java
index 7a91e97..768d197 100644
--- a/src/main/java/org/apache/cassandra/sidecar/config/SslConfiguration.java
+++ b/src/main/java/org/apache/cassandra/sidecar/config/SslConfiguration.java
@@ -29,6 +29,31 @@
     boolean enabled();
 
     /**
+     * Returns {@code true} if the OpenSSL engine should be preferred, {@code false} otherwise.
+     *
+     * <br><b>Note:</b> The OpenSSL engine will only be enabled if the native libraries for OpenSSL have
+     * been loaded correctly.
+     *
+     * @return {@code true} if the OpenSSL engine should be used, {@code false} otherwise
+     */
+    boolean preferOpenSSL();
+
+    /**
+     * @return the configuration for the SSL handshake timeout in seconds
+     */
+    long handshakeTimeoutInSeconds();
+
+    /**
+     * Returns the client authentication mode. Valid values are {@code NONE}, {@code REQUEST}, and {@code REQUIRED}.
+     * When the authentication mode is set to {@code REQUIRED} then server will require the SSL certificate to be
+     * presented, otherwise it won't accept the request. When the authentication mode is set to {@code REQUEST}, the
+     * certificate is optional.
+     *
+     * @return the client authentication mode
+     */
+    String clientAuth();
+
+    /**
      * @return {@code true} if the keystore is configured, and the {@link KeyStoreConfiguration#path()} and
      * {@link KeyStoreConfiguration#password()} parameters are provided
      */
@@ -46,7 +71,7 @@
      * @return {@code true} if the truststore is configured, and the {@link KeyStoreConfiguration#path()} and
      * {@link KeyStoreConfiguration#password()} parameters are provided
      */
-    default boolean isTruststoreConfigured()
+    default boolean isTrustStoreConfigured()
     {
         return truststore() != null && truststore().isConfigured();
     }
diff --git a/src/main/java/org/apache/cassandra/sidecar/config/TrafficShapingConfiguration.java b/src/main/java/org/apache/cassandra/sidecar/config/TrafficShapingConfiguration.java
new file mode 100644
index 0000000..61b1541
--- /dev/null
+++ b/src/main/java/org/apache/cassandra/sidecar/config/TrafficShapingConfiguration.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.config;
+
+/**
+ * Configuration for the global traffic shaping options. These TCP server options enable configuration of
+ * bandwidth limiting. Both inbound and outbound bandwidth can be limited through these options.
+ */
+public interface TrafficShapingConfiguration
+{
+    /**
+     * @return the bandwidth limit in bytes per second for inbound connections
+     */
+    long inboundGlobalBandwidthBytesPerSecond();
+
+    /**
+     * @return the bandwidth limit in bytes per second for outbound connections
+     */
+    long outboundGlobalBandwidthBytesPerSecond();
+
+    /**
+     * @return the maximum global write size in bytes per second allowed in the buffer globally for all channels
+     * before write suspended is set
+     */
+    long peakOutboundGlobalBandwidthBytesPerSecond();
+
+    /**
+     * @return the maximum delay to wait in case of traffic excess in milliseconds
+     */
+    long maxDelayToWaitMillis();
+
+    /**
+     * @return the delay in milliseconds between two computations of performances for channels or {@code 0} if no
+     * stats are to be computed
+     */
+    long checkIntervalForStatsMillis();
+
+    /**
+     * @return the bandwidth limit in bytes per second for incoming files (i.e. SSTable components upload), this
+     * setting is upper-bounded by the {@link #inboundGlobalBandwidthBytesPerSecond()} configuration if throttled
+     */
+    long inboundGlobalFileBandwidthBytesPerSecond();
+}
diff --git a/src/main/java/org/apache/cassandra/sidecar/config/yaml/HealthCheckConfigurationImpl.java b/src/main/java/org/apache/cassandra/sidecar/config/yaml/HealthCheckConfigurationImpl.java
index bf96cd8..2298bb5 100644
--- a/src/main/java/org/apache/cassandra/sidecar/config/yaml/HealthCheckConfigurationImpl.java
+++ b/src/main/java/org/apache/cassandra/sidecar/config/yaml/HealthCheckConfigurationImpl.java
@@ -27,24 +27,40 @@
  */
 public class HealthCheckConfigurationImpl implements HealthCheckConfiguration
 {
+    public static final String INITIAL_DELAY_MILLIS_PROPERTY = "initial_delay_millis";
+    public static final int DEFAULT_INITIAL_DELAY_MILLIS = 0;
+
     public static final String POLL_FREQ_MILLIS_PROPERTY = "poll_freq_millis";
     public static final int DEFAULT_CHECK_INTERVAL_MILLIS = 30000;
 
+    @JsonProperty(value = INITIAL_DELAY_MILLIS_PROPERTY, defaultValue = DEFAULT_INITIAL_DELAY_MILLIS + "")
+    protected final int initialDelayMillis;
+
     @JsonProperty(value = POLL_FREQ_MILLIS_PROPERTY, defaultValue = DEFAULT_CHECK_INTERVAL_MILLIS + "")
     protected final int checkIntervalMillis;
 
     public HealthCheckConfigurationImpl()
     {
-        this(DEFAULT_CHECK_INTERVAL_MILLIS);
+        this(DEFAULT_INITIAL_DELAY_MILLIS, DEFAULT_CHECK_INTERVAL_MILLIS);
     }
 
     @VisibleForTesting
-    public HealthCheckConfigurationImpl(int checkIntervalMillis)
+    public HealthCheckConfigurationImpl(int initialDelayMillis, int checkIntervalMillis)
     {
+        this.initialDelayMillis = initialDelayMillis;
         this.checkIntervalMillis = checkIntervalMillis;
     }
 
     /**
+     * @return the initial delay for the first health check, in milliseconds
+     */
+    @Override
+    public int initialDelayMillis()
+    {
+        return initialDelayMillis;
+    }
+
+    /**
      * @return the interval, in milliseconds, in which the health checks will be performed
      */
     @Override
diff --git a/src/main/java/org/apache/cassandra/sidecar/config/yaml/KeyStoreConfigurationImpl.java b/src/main/java/org/apache/cassandra/sidecar/config/yaml/KeyStoreConfigurationImpl.java
index 450f1eb..84a5fcb 100644
--- a/src/main/java/org/apache/cassandra/sidecar/config/yaml/KeyStoreConfigurationImpl.java
+++ b/src/main/java/org/apache/cassandra/sidecar/config/yaml/KeyStoreConfigurationImpl.java
@@ -26,8 +26,6 @@
  */
 public class KeyStoreConfigurationImpl implements KeyStoreConfiguration
 {
-    public static final String DEFAULT_TYPE = "JKS";
-
     @JsonProperty("path")
     protected final String path;
 
@@ -37,21 +35,25 @@
     @JsonProperty(value = "type", defaultValue = DEFAULT_TYPE)
     protected final String type;
 
+    @JsonProperty(value = "check_interval_sec", defaultValue = DEFAULT_CHECK_INTERVAL_SECONDS + "")
+    protected final int checkIntervalInSeconds;
+
     public KeyStoreConfigurationImpl()
     {
-        this(null, null, DEFAULT_TYPE);
+        this(null, null, DEFAULT_TYPE, DEFAULT_CHECK_INTERVAL_SECONDS);
     }
 
     public KeyStoreConfigurationImpl(String path, String password)
     {
-        this(path, password, DEFAULT_TYPE);
+        this(path, password, DEFAULT_TYPE, DEFAULT_CHECK_INTERVAL_SECONDS);
     }
 
-    public KeyStoreConfigurationImpl(String path, String password, String type)
+    public KeyStoreConfigurationImpl(String path, String password, String type, int checkIntervalInSeconds)
     {
         this.path = path;
         this.password = password;
         this.type = type;
+        this.checkIntervalInSeconds = checkIntervalInSeconds;
     }
 
     /**
@@ -78,9 +80,19 @@
      * @return the type of the store
      */
     @Override
-    @JsonProperty("type")
+    @JsonProperty(value = "type", defaultValue = DEFAULT_TYPE)
     public String type()
     {
         return type;
     }
+
+    /**
+     * @return the interval, in seconds, in which the key store will be checked for changes in the filesystem
+     */
+    @Override
+    @JsonProperty(value = "check_interval_sec", defaultValue = DEFAULT_CHECK_INTERVAL_SECONDS + "")
+    public int checkIntervalInSeconds()
+    {
+        return checkIntervalInSeconds;
+    }
 }
diff --git a/src/main/java/org/apache/cassandra/sidecar/config/yaml/ServiceConfigurationImpl.java b/src/main/java/org/apache/cassandra/sidecar/config/yaml/ServiceConfigurationImpl.java
index 9b829e5..220f859 100644
--- a/src/main/java/org/apache/cassandra/sidecar/config/yaml/ServiceConfigurationImpl.java
+++ b/src/main/java/org/apache/cassandra/sidecar/config/yaml/ServiceConfigurationImpl.java
@@ -24,11 +24,13 @@
 import java.util.concurrent.TimeUnit;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.cassandra.sidecar.common.DataObjectBuilder;
 import org.apache.cassandra.sidecar.config.JmxConfiguration;
 import org.apache.cassandra.sidecar.config.SSTableImportConfiguration;
 import org.apache.cassandra.sidecar.config.SSTableUploadConfiguration;
 import org.apache.cassandra.sidecar.config.ServiceConfiguration;
 import org.apache.cassandra.sidecar.config.ThrottleConfiguration;
+import org.apache.cassandra.sidecar.config.TrafficShapingConfiguration;
 import org.apache.cassandra.sidecar.config.WorkerPoolConfiguration;
 
 /**
@@ -44,12 +46,20 @@
     public static final int DEFAULT_REQUEST_IDLE_TIMEOUT_MILLIS = 300000;
     public static final String REQUEST_TIMEOUT_MILLIS_PROPERTY = "request_timeout_millis";
     public static final long DEFAULT_REQUEST_TIMEOUT_MILLIS = 300000L;
+    public static final String TCP_KEEP_ALIVE_PROPERTY = "tcp_keep_alive";
+    public static final boolean DEFAULT_TCP_KEEP_ALIVE = false;
+    public static final String ACCEPT_BACKLOG_PROPERTY = "accept_backlog";
+    public static final int DEFAULT_ACCEPT_BACKLOG = 1024;
     public static final String ALLOWABLE_SKEW_IN_MINUTES_PROPERTY = "allowable_time_skew_in_minutes";
     public static final int DEFAULT_ALLOWABLE_SKEW_IN_MINUTES = 60;
+    private static final String SERVER_VERTICLE_INSTANCES_PROPERTY = "server_verticle_instances";
+    private static final int DEFAULT_SERVER_VERTICLE_INSTANCES = 1;
     public static final String THROTTLE_PROPERTY = "throttle";
     public static final String SSTABLE_UPLOAD_PROPERTY = "sstable_upload";
     public static final String SSTABLE_IMPORT_PROPERTY = "sstable_import";
     public static final String WORKER_POOLS_PROPERTY = "worker_pools";
+    private static final String JMX_PROPERTY = "jmx";
+    private static final String TRAFFIC_SHAPING_PROPERTY = "traffic_shaping";
     protected static final Map<String, WorkerPoolConfiguration> DEFAULT_WORKER_POOLS_CONFIGURATION
     = Collections.unmodifiableMap(new HashMap<String, WorkerPoolConfiguration>()
     {{
@@ -73,9 +83,18 @@
     @JsonProperty(value = REQUEST_TIMEOUT_MILLIS_PROPERTY, defaultValue = DEFAULT_REQUEST_TIMEOUT_MILLIS + "")
     protected final long requestTimeoutMillis;
 
+    @JsonProperty(value = TCP_KEEP_ALIVE_PROPERTY, defaultValue = DEFAULT_TCP_KEEP_ALIVE + "")
+    protected final boolean tcpKeepAlive;
+
+    @JsonProperty(value = ACCEPT_BACKLOG_PROPERTY, defaultValue = DEFAULT_ACCEPT_BACKLOG + "")
+    protected final int acceptBacklog;
+
     @JsonProperty(value = ALLOWABLE_SKEW_IN_MINUTES_PROPERTY, defaultValue = DEFAULT_ALLOWABLE_SKEW_IN_MINUTES + "")
     protected final int allowableSkewInMinutes;
 
+    @JsonProperty(value = SERVER_VERTICLE_INSTANCES_PROPERTY, defaultValue = DEFAULT_SERVER_VERTICLE_INSTANCES + "")
+    protected final int serverVerticleInstances;
+
     @JsonProperty(value = THROTTLE_PROPERTY, required = true)
     protected final ThrottleConfiguration throttleConfiguration;
 
@@ -88,117 +107,45 @@
     @JsonProperty(value = WORKER_POOLS_PROPERTY, required = true)
     protected final Map<String, ? extends WorkerPoolConfiguration> workerPoolsConfiguration;
 
-    @JsonProperty("jmx")
+    @JsonProperty(value = JMX_PROPERTY)
     protected final JmxConfiguration jmxConfiguration;
 
+    @JsonProperty(value = TRAFFIC_SHAPING_PROPERTY)
+    protected final TrafficShapingConfiguration trafficShapingConfiguration;
+
+    /**
+     * Constructs a new {@link ServiceConfigurationImpl} with the default values
+     */
     public ServiceConfigurationImpl()
     {
-        this(DEFAULT_HOST,
-             DEFAULT_PORT,
-             DEFAULT_REQUEST_IDLE_TIMEOUT_MILLIS,
-             DEFAULT_REQUEST_TIMEOUT_MILLIS,
-             DEFAULT_ALLOWABLE_SKEW_IN_MINUTES,
-             new ThrottleConfigurationImpl(),
-             new SSTableUploadConfigurationImpl(),
-             new SSTableImportConfigurationImpl(),
-             DEFAULT_WORKER_POOLS_CONFIGURATION,
-             new JmxConfigurationImpl());
-    }
-
-    public ServiceConfigurationImpl(SSTableImportConfiguration ssTableImportConfiguration)
-    {
-        this(DEFAULT_HOST,
-             DEFAULT_PORT,
-             DEFAULT_REQUEST_IDLE_TIMEOUT_MILLIS,
-             DEFAULT_REQUEST_TIMEOUT_MILLIS,
-             DEFAULT_ALLOWABLE_SKEW_IN_MINUTES,
-             new ThrottleConfigurationImpl(),
-             new SSTableUploadConfigurationImpl(),
-             ssTableImportConfiguration,
-             DEFAULT_WORKER_POOLS_CONFIGURATION,
-             new JmxConfigurationImpl());
-    }
-
-    public ServiceConfigurationImpl(String host,
-                                    ThrottleConfiguration throttleConfiguration,
-                                    SSTableUploadConfiguration ssTableUploadConfiguration,
-                                    JmxConfiguration jmxConfiguration)
-    {
-        this(host,
-             DEFAULT_PORT,
-             DEFAULT_REQUEST_IDLE_TIMEOUT_MILLIS,
-             DEFAULT_REQUEST_TIMEOUT_MILLIS,
-             DEFAULT_ALLOWABLE_SKEW_IN_MINUTES,
-             throttleConfiguration,
-             ssTableUploadConfiguration,
-             new SSTableImportConfigurationImpl(),
-             DEFAULT_WORKER_POOLS_CONFIGURATION,
-             jmxConfiguration);
-    }
-
-    public ServiceConfigurationImpl(int requestIdleTimeoutMillis,
-                                    long requestTimeoutMillis,
-                                    SSTableUploadConfiguration ssTableUploadConfiguration)
-    {
-
-        this(DEFAULT_HOST,
-             DEFAULT_PORT,
-             requestIdleTimeoutMillis,
-             requestTimeoutMillis,
-             DEFAULT_ALLOWABLE_SKEW_IN_MINUTES,
-             new ThrottleConfigurationImpl(),
-             ssTableUploadConfiguration,
-             new SSTableImportConfigurationImpl(),
-             DEFAULT_WORKER_POOLS_CONFIGURATION,
-             new JmxConfigurationImpl());
-    }
-
-    public ServiceConfigurationImpl(String host,
-                                    int port,
-                                    int requestIdleTimeoutMillis,
-                                    long requestTimeoutMillis,
-                                    int allowableSkewInMinutes,
-                                    ThrottleConfiguration throttleConfiguration,
-                                    SSTableUploadConfiguration ssTableUploadConfiguration,
-                                    SSTableImportConfiguration ssTableImportConfiguration,
-                                    Map<String, ? extends WorkerPoolConfiguration> workerPoolsConfiguration,
-                                    JmxConfiguration jmxConfiguration)
-    {
-        this.host = host;
-        this.port = port;
-        this.requestIdleTimeoutMillis = requestIdleTimeoutMillis;
-        this.requestTimeoutMillis = requestTimeoutMillis;
-        this.allowableSkewInMinutes = allowableSkewInMinutes;
-        this.throttleConfiguration = throttleConfiguration;
-        this.ssTableUploadConfiguration = ssTableUploadConfiguration;
-        this.ssTableImportConfiguration = ssTableImportConfiguration;
-        this.jmxConfiguration = jmxConfiguration;
-        if (workerPoolsConfiguration == null || workerPoolsConfiguration.isEmpty())
-        {
-            this.workerPoolsConfiguration = DEFAULT_WORKER_POOLS_CONFIGURATION;
-        }
-        else
-        {
-            this.workerPoolsConfiguration = workerPoolsConfiguration;
-        }
-    }
-
-    public ServiceConfigurationImpl(String host)
-    {
-        this(host,
-             DEFAULT_PORT,
-             DEFAULT_REQUEST_IDLE_TIMEOUT_MILLIS,
-             DEFAULT_REQUEST_TIMEOUT_MILLIS,
-             DEFAULT_ALLOWABLE_SKEW_IN_MINUTES,
-             new ThrottleConfigurationImpl(),
-             new SSTableUploadConfigurationImpl(),
-             new SSTableImportConfigurationImpl(),
-             DEFAULT_WORKER_POOLS_CONFIGURATION,
-             new JmxConfigurationImpl());
+        this(builder());
     }
 
     /**
-     * Sidecar's HTTP REST API listen address
+     * Constructs a new {@link ServiceConfigurationImpl} with the configured {@link Builder}
+     *
+     * @param builder the builder object
+     */
+    protected ServiceConfigurationImpl(Builder builder)
+    {
+        host = builder.host;
+        port = builder.port;
+        requestIdleTimeoutMillis = builder.requestIdleTimeoutMillis;
+        requestTimeoutMillis = builder.requestTimeoutMillis;
+        tcpKeepAlive = builder.tcpKeepAlive;
+        acceptBacklog = builder.acceptBacklog;
+        allowableSkewInMinutes = builder.allowableSkewInMinutes;
+        serverVerticleInstances = builder.serverVerticleInstances;
+        throttleConfiguration = builder.throttleConfiguration;
+        ssTableUploadConfiguration = builder.ssTableUploadConfiguration;
+        ssTableImportConfiguration = builder.ssTableImportConfiguration;
+        workerPoolsConfiguration = builder.workerPoolsConfiguration;
+        jmxConfiguration = builder.jmxConfiguration;
+        trafficShapingConfiguration = builder.trafficShapingConfiguration;
+    }
+
+    /**
+     * {@inheritDoc}
      */
     @Override
     @JsonProperty(value = HOST_PROPERTY, defaultValue = DEFAULT_HOST)
@@ -208,7 +155,7 @@
     }
 
     /**
-     * @return Sidecar's HTTP REST API port
+     * {@inheritDoc}
      */
     @Override
     @JsonProperty(value = PORT_PROPERTY, defaultValue = DEFAULT_PORT + "")
@@ -218,10 +165,7 @@
     }
 
     /**
-     * Determines if a connection will timeout and be closed if no data is received nor sent within the timeout.
-     * Zero means don't timeout.
-     *
-     * @return the configured idle timeout value
+     * {@inheritDoc}
      */
     @Override
     @JsonProperty(value = REQUEST_IDLE_TIMEOUT_MILLIS_PROPERTY, defaultValue = DEFAULT_REQUEST_IDLE_TIMEOUT_MILLIS + "")
@@ -231,7 +175,7 @@
     }
 
     /**
-     * Determines if a response will timeout if the response has not been written after a certain time.
+     * {@inheritDoc}
      */
     @Override
     @JsonProperty(value = REQUEST_TIMEOUT_MILLIS_PROPERTY, defaultValue = DEFAULT_REQUEST_TIMEOUT_MILLIS + "")
@@ -241,7 +185,27 @@
     }
 
     /**
-     * @return the maximum time skew allowed between the server and the client
+     * {@inheritDoc}
+     */
+    @Override
+    @JsonProperty(value = TCP_KEEP_ALIVE_PROPERTY, defaultValue = DEFAULT_TCP_KEEP_ALIVE + "")
+    public boolean tcpKeepAlive()
+    {
+        return tcpKeepAlive;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    @JsonProperty(value = ACCEPT_BACKLOG_PROPERTY, defaultValue = DEFAULT_ACCEPT_BACKLOG + "")
+    public int acceptBacklog()
+    {
+        return acceptBacklog;
+    }
+
+    /**
+     * {@inheritDoc}
      */
     @Override
     @JsonProperty(value = ALLOWABLE_SKEW_IN_MINUTES_PROPERTY, defaultValue = DEFAULT_ALLOWABLE_SKEW_IN_MINUTES + "")
@@ -251,7 +215,17 @@
     }
 
     /**
-     * @return the throttling configuration
+     * {@inheritDoc}
+     */
+    @Override
+    @JsonProperty(value = SERVER_VERTICLE_INSTANCES_PROPERTY, defaultValue = DEFAULT_SERVER_VERTICLE_INSTANCES + "")
+    public int serverVerticleInstances()
+    {
+        return serverVerticleInstances;
+    }
+
+    /**
+     * {@inheritDoc}
      */
     @Override
     @JsonProperty(value = THROTTLE_PROPERTY, required = true)
@@ -261,7 +235,7 @@
     }
 
     /**
-     * @return the configuration for SSTable component uploads on this service
+     * {@inheritDoc}
      */
     @Override
     @JsonProperty(value = SSTABLE_UPLOAD_PROPERTY, required = true)
@@ -271,7 +245,7 @@
     }
 
     /**
-     * @return the configuration for the SSTable Import functionality
+     * {@inheritDoc}
      */
     @Override
     @JsonProperty(value = SSTABLE_IMPORT_PROPERTY, required = true)
@@ -281,7 +255,7 @@
     }
 
     /**
-     * @return the configured worker pools for the service
+     * {@inheritDoc}
      */
     @Override
     @JsonProperty(value = WORKER_POOLS_PROPERTY, required = true)
@@ -291,13 +265,226 @@
     }
 
     /**
-     * @return the general JMX configuration options (not per-instance)
+     * {@inheritDoc}
      */
     @Override
-    @JsonProperty("jmx")
+    @JsonProperty(value = JMX_PROPERTY)
     public JmxConfiguration jmxConfiguration()
     {
         return jmxConfiguration;
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    @JsonProperty(value = TRAFFIC_SHAPING_PROPERTY)
+    public TrafficShapingConfiguration trafficShapingConfiguration()
+    {
+        return trafficShapingConfiguration;
+    }
+
+    public static Builder builder()
+    {
+        return new Builder();
+    }
+
+    /**
+     * {@code ServiceConfigurationImpl} builder static inner class.
+     */
+    public static class Builder implements DataObjectBuilder<Builder, ServiceConfigurationImpl>
+    {
+        protected String host = DEFAULT_HOST;
+        protected int port = DEFAULT_PORT;
+        protected int requestIdleTimeoutMillis = DEFAULT_REQUEST_IDLE_TIMEOUT_MILLIS;
+        protected long requestTimeoutMillis = DEFAULT_REQUEST_TIMEOUT_MILLIS;
+        protected boolean tcpKeepAlive = DEFAULT_TCP_KEEP_ALIVE;
+        protected int acceptBacklog = DEFAULT_ACCEPT_BACKLOG;
+        protected int allowableSkewInMinutes = DEFAULT_ALLOWABLE_SKEW_IN_MINUTES;
+        protected int serverVerticleInstances = DEFAULT_SERVER_VERTICLE_INSTANCES;
+        protected ThrottleConfiguration throttleConfiguration = new ThrottleConfigurationImpl();
+        protected SSTableUploadConfiguration ssTableUploadConfiguration = new SSTableUploadConfigurationImpl();
+        protected SSTableImportConfiguration ssTableImportConfiguration = new SSTableImportConfigurationImpl();
+        protected Map<String, ? extends WorkerPoolConfiguration> workerPoolsConfiguration =
+        DEFAULT_WORKER_POOLS_CONFIGURATION;
+        protected JmxConfiguration jmxConfiguration = new JmxConfigurationImpl();
+        protected TrafficShapingConfiguration trafficShapingConfiguration = new TrafficShapingConfigurationImpl();
+
+        private Builder()
+        {
+        }
+
+        @Override
+        public Builder self()
+        {
+            return this;
+        }
+
+        /**
+         * Sets the {@code host} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param host the {@code host} to set
+         * @return a reference to this Builder
+         */
+        public Builder host(String host)
+        {
+            return update(b -> b.host = host);
+        }
+
+        /**
+         * Sets the {@code port} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param port the {@code port} to set
+         * @return a reference to this Builder
+         */
+        public Builder port(int port)
+        {
+            return update(b -> b.port = port);
+        }
+
+        /**
+         * Sets the {@code requestIdleTimeoutMillis} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param requestIdleTimeoutMillis the {@code requestIdleTimeoutMillis} to set
+         * @return a reference to this Builder
+         */
+        public Builder requestIdleTimeoutMillis(int requestIdleTimeoutMillis)
+        {
+            return update(b -> b.requestIdleTimeoutMillis = requestIdleTimeoutMillis);
+        }
+
+        /**
+         * Sets the {@code requestTimeoutMillis} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param requestTimeoutMillis the {@code requestTimeoutMillis} to set
+         * @return a reference to this Builder
+         */
+        public Builder requestTimeoutMillis(long requestTimeoutMillis)
+        {
+            return update(b -> b.requestTimeoutMillis = requestTimeoutMillis);
+        }
+
+        /**
+         * Sets the {@code tcpKeepAlive} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param tcpKeepAlive the {@code tcpKeepAlive} to set
+         * @return a reference to this Builder
+         */
+        public Builder tcpKeepAlive(boolean tcpKeepAlive)
+        {
+            return update(b -> b.tcpKeepAlive = tcpKeepAlive);
+        }
+
+        /**
+         * Sets the {@code acceptBacklog} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param acceptBacklog the {@code acceptBacklog} to set
+         * @return a reference to this Builder
+         */
+        public Builder acceptBacklog(int acceptBacklog)
+        {
+            return update(b -> b.acceptBacklog = acceptBacklog);
+        }
+
+        /**
+         * Sets the {@code allowableSkewInMinutes} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param allowableSkewInMinutes the {@code allowableSkewInMinutes} to set
+         * @return a reference to this Builder
+         */
+        public Builder allowableSkewInMinutes(int allowableSkewInMinutes)
+        {
+            return update(b -> b.allowableSkewInMinutes = allowableSkewInMinutes);
+        }
+
+        /**
+         * Sets the {@code serverVerticleInstances} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param serverVerticleInstances the {@code serverVerticleInstances} to set
+         * @return a reference to this Builder
+         */
+        public Builder serverVerticleInstances(int serverVerticleInstances)
+        {
+            return update(b -> b.serverVerticleInstances = serverVerticleInstances);
+        }
+
+        /**
+         * Sets the {@code throttleConfiguration} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param throttleConfiguration the {@code throttleConfiguration} to set
+         * @return a reference to this Builder
+         */
+        public Builder throttleConfiguration(ThrottleConfiguration throttleConfiguration)
+        {
+            return update(b -> b.throttleConfiguration = throttleConfiguration);
+        }
+
+        /**
+         * Sets the {@code ssTableUploadConfiguration} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param ssTableUploadConfiguration the {@code ssTableUploadConfiguration} to set
+         * @return a reference to this Builder
+         */
+        public Builder ssTableUploadConfiguration(SSTableUploadConfiguration ssTableUploadConfiguration)
+        {
+            return update(b -> b.ssTableUploadConfiguration = ssTableUploadConfiguration);
+        }
+
+        /**
+         * Sets the {@code ssTableImportConfiguration} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param ssTableImportConfiguration the {@code ssTableImportConfiguration} to set
+         * @return a reference to this Builder
+         */
+        public Builder ssTableImportConfiguration(SSTableImportConfiguration ssTableImportConfiguration)
+        {
+            return update(b -> b.ssTableImportConfiguration = ssTableImportConfiguration);
+        }
+
+        /**
+         * Sets the {@code workerPoolsConfiguration} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param workerPoolsConfiguration the {@code workerPoolsConfiguration} to set
+         * @return a reference to this Builder
+         */
+        public Builder workerPoolsConfiguration(Map<String, ? extends WorkerPoolConfiguration> workerPoolsConfiguration)
+        {
+            return update(b -> b.workerPoolsConfiguration = workerPoolsConfiguration);
+        }
+
+        /**
+         * Sets the {@code jmxConfiguration} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param jmxConfiguration the {@code jmxConfiguration} to set
+         * @return a reference to this Builder
+         */
+        public Builder jmxConfiguration(JmxConfiguration jmxConfiguration)
+        {
+            return update(b -> b.jmxConfiguration = jmxConfiguration);
+        }
+
+        /**
+         * Sets the {@code trafficShapingConfiguration} and returns a reference to this Builder enabling method
+         * chaining.
+         *
+         * @param trafficShapingConfiguration the {@code trafficShapingConfiguration} to set
+         * @return a reference to this Builder
+         */
+        public Builder trafficShapingConfiguration(TrafficShapingConfiguration trafficShapingConfiguration)
+        {
+            return update(b -> b.trafficShapingConfiguration = trafficShapingConfiguration);
+        }
+
+        /**
+         * Returns a {@code ServiceConfigurationImpl} built from the parameters previously set.
+         *
+         * @return a {@code ServiceConfigurationImpl} built with parameters of this
+         * {@code ServiceConfigurationImpl.Builder}
+         */
+        @Override
+        public ServiceConfigurationImpl build()
+        {
+            return new ServiceConfigurationImpl(this);
+        }
+    }
 }
diff --git a/src/main/java/org/apache/cassandra/sidecar/config/yaml/SidecarConfigurationImpl.java b/src/main/java/org/apache/cassandra/sidecar/config/yaml/SidecarConfigurationImpl.java
index 8ee8c37..4219443 100644
--- a/src/main/java/org/apache/cassandra/sidecar/config/yaml/SidecarConfigurationImpl.java
+++ b/src/main/java/org/apache/cassandra/sidecar/config/yaml/SidecarConfigurationImpl.java
@@ -19,6 +19,8 @@
 package org.apache.cassandra.sidecar.config.yaml;
 
 import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Collections;
@@ -29,6 +31,7 @@
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.module.SimpleModule;
 import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import org.apache.cassandra.sidecar.common.DataObjectBuilder;
 import org.apache.cassandra.sidecar.config.CacheConfiguration;
 import org.apache.cassandra.sidecar.config.CassandraInputValidationConfiguration;
 import org.apache.cassandra.sidecar.config.HealthCheckConfiguration;
@@ -41,6 +44,7 @@
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
 import org.apache.cassandra.sidecar.config.SslConfiguration;
 import org.apache.cassandra.sidecar.config.ThrottleConfiguration;
+import org.apache.cassandra.sidecar.config.TrafficShapingConfiguration;
 import org.apache.cassandra.sidecar.config.WorkerPoolConfiguration;
 
 /**
@@ -69,45 +73,17 @@
 
     public SidecarConfigurationImpl()
     {
-        this(Collections.emptyList(),
-             new ServiceConfigurationImpl(),
-             null /* sslConfiguration */,
-             new HealthCheckConfigurationImpl(),
-             new CassandraInputValidationConfigurationImpl());
+        this(builder());
     }
 
-    public SidecarConfigurationImpl(ServiceConfiguration serviceConfiguration)
+    protected SidecarConfigurationImpl(Builder builder)
     {
-        this(Collections.emptyList(),
-             serviceConfiguration,
-             null /* sslConfiguration */,
-             new HealthCheckConfigurationImpl(),
-             new CassandraInputValidationConfigurationImpl());
-    }
-
-    public SidecarConfigurationImpl(ServiceConfiguration serviceConfiguration,
-                                    SslConfiguration sslConfiguration,
-                                    HealthCheckConfiguration healthCheckConfiguration)
-    {
-        this(Collections.emptyList(),
-             serviceConfiguration,
-             sslConfiguration,
-             healthCheckConfiguration,
-             new CassandraInputValidationConfigurationImpl());
-    }
-
-    public SidecarConfigurationImpl(List<InstanceConfiguration> cassandraInstances,
-                                    ServiceConfiguration serviceConfiguration,
-                                    SslConfiguration sslConfiguration,
-                                    HealthCheckConfiguration healthCheckConfiguration,
-                                    CassandraInputValidationConfiguration cassandraInputValidationConfiguration)
-    {
-        this.cassandraInstance = null;
-        this.cassandraInstances = Collections.unmodifiableList(cassandraInstances);
-        this.serviceConfiguration = serviceConfiguration;
-        this.sslConfiguration = sslConfiguration;
-        this.healthCheckConfiguration = healthCheckConfiguration;
-        this.cassandraInputValidationConfiguration = cassandraInputValidationConfiguration;
+        cassandraInstance = builder.cassandraInstance;
+        cassandraInstances = builder.cassandraInstances;
+        serviceConfiguration = builder.serviceConfiguration;
+        sslConfiguration = builder.sslConfiguration;
+        healthCheckConfiguration = builder.healthCheckConfiguration;
+        cassandraInputValidationConfiguration = builder.cassandraInputValidationConfiguration;
     }
 
     /**
@@ -182,7 +158,14 @@
 
     public static SidecarConfigurationImpl readYamlConfiguration(String yamlConfigurationPath) throws IOException
     {
-        return readYamlConfiguration(Paths.get(yamlConfigurationPath));
+        try
+        {
+            return readYamlConfiguration(Paths.get(new URI(yamlConfigurationPath)));
+        }
+        catch (URISyntaxException e)
+        {
+            throw new IOException("Invalid URI: " + yamlConfigurationPath, e);
+        }
     }
 
     public static SidecarConfigurationImpl readYamlConfiguration(Path yamlConfigurationPath) throws IOException
@@ -213,7 +196,9 @@
                                     .addAbstractTypeMapping(WorkerPoolConfiguration.class,
                                                             WorkerPoolConfigurationImpl.class)
                                     .addAbstractTypeMapping(JmxConfiguration.class,
-                                                            JmxConfigurationImpl.class);
+                                                            JmxConfigurationImpl.class)
+                                    .addAbstractTypeMapping(TrafficShapingConfiguration.class,
+                                                            TrafficShapingConfigurationImpl.class);
 
         ObjectMapper mapper = new ObjectMapper(new YAMLFactory())
                               .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
@@ -223,4 +208,112 @@
 
         return mapper.readValue(yamlConfigurationPath.toFile(), SidecarConfigurationImpl.class);
     }
+
+    public static Builder builder()
+    {
+        return new Builder();
+    }
+
+    /**
+     * {@code SidecarConfigurationImpl} builder static inner class.
+     */
+    public static class Builder implements DataObjectBuilder<Builder, SidecarConfigurationImpl>
+    {
+        private InstanceConfiguration cassandraInstance;
+        private List<InstanceConfiguration> cassandraInstances;
+        private ServiceConfiguration serviceConfiguration = new ServiceConfigurationImpl();
+        private SslConfiguration sslConfiguration = null;
+        private HealthCheckConfiguration healthCheckConfiguration = new HealthCheckConfigurationImpl();
+        private CassandraInputValidationConfiguration cassandraInputValidationConfiguration
+        = new CassandraInputValidationConfigurationImpl();
+
+        protected Builder()
+        {
+        }
+
+        @Override
+        public Builder self()
+        {
+            return this;
+        }
+
+        /**
+         * Sets the {@code cassandraInstance} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param cassandraInstance the {@code cassandraInstance} to set
+         * @return a reference to this Builder
+         */
+        public Builder cassandraInstance(InstanceConfiguration cassandraInstance)
+        {
+            return update(b -> b.cassandraInstance = cassandraInstance);
+        }
+
+        /**
+         * Sets the {@code cassandraInstances} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param cassandraInstances the {@code cassandraInstances} to set
+         * @return a reference to this Builder
+         */
+        public Builder cassandraInstances(List<InstanceConfiguration> cassandraInstances)
+        {
+            return update(b -> b.cassandraInstances = cassandraInstances);
+        }
+
+        /**
+         * Sets the {@code serviceConfiguration} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param serviceConfiguration the {@code serviceConfiguration} to set
+         * @return a reference to this Builder
+         */
+        public Builder serviceConfiguration(ServiceConfiguration serviceConfiguration)
+        {
+            return update(b -> b.serviceConfiguration = serviceConfiguration);
+        }
+
+        /**
+         * Sets the {@code sslConfiguration} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param sslConfiguration the {@code sslConfiguration} to set
+         * @return a reference to this Builder
+         */
+        public Builder sslConfiguration(SslConfiguration sslConfiguration)
+        {
+            return update(b -> b.sslConfiguration = sslConfiguration);
+        }
+
+        /**
+         * Sets the {@code healthCheckConfiguration} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param healthCheckConfiguration the {@code healthCheckConfiguration} to set
+         * @return a reference to this Builder
+         */
+        public Builder healthCheckConfiguration(HealthCheckConfiguration healthCheckConfiguration)
+        {
+            return update(b -> b.healthCheckConfiguration = healthCheckConfiguration);
+        }
+
+        /**
+         * Sets the {@code cassandraInputValidationConfiguration} and returns a reference to this Builder enabling
+         * method chaining.
+         *
+         * @param configuration the {@code cassandraInputValidationConfiguration} to set
+         * @return a reference to this Builder
+         */
+        public Builder cassandraInputValidationConfiguration(CassandraInputValidationConfiguration configuration)
+        {
+            return update(b -> b.cassandraInputValidationConfiguration = configuration);
+        }
+
+        /**
+         * Returns a {@code SidecarConfigurationImpl} built from the parameters previously set.
+         *
+         * @return a {@code SidecarConfigurationImpl} built with parameters of this
+         * {@code SidecarConfigurationImpl.Builder}
+         */
+        @Override
+        public SidecarConfigurationImpl build()
+        {
+            return new SidecarConfigurationImpl(this);
+        }
+    }
 }
diff --git a/src/main/java/org/apache/cassandra/sidecar/config/yaml/SslConfigurationImpl.java b/src/main/java/org/apache/cassandra/sidecar/config/yaml/SslConfigurationImpl.java
index 49ef5f6..fbb7677 100644
--- a/src/main/java/org/apache/cassandra/sidecar/config/yaml/SslConfigurationImpl.java
+++ b/src/main/java/org/apache/cassandra/sidecar/config/yaml/SslConfigurationImpl.java
@@ -18,7 +18,12 @@
 
 package org.apache.cassandra.sidecar.config.yaml;
 
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
 import com.fasterxml.jackson.annotation.JsonProperty;
+import io.vertx.core.http.ClientAuth;
+import org.apache.cassandra.sidecar.common.DataObjectBuilder;
 import org.apache.cassandra.sidecar.config.KeyStoreConfiguration;
 import org.apache.cassandra.sidecar.config.SslConfiguration;
 
@@ -27,9 +32,23 @@
  */
 public class SslConfigurationImpl implements SslConfiguration
 {
+    public static final boolean DEFAULT_SSL_ENABLED = false;
+    public static final boolean DEFAULT_USE_OPEN_SSL = true;
+    public static final long DEFAULT_HANDSHAKE_TIMEOUT_SECONDS = 10L;
+    public static final String DEFAULT_CLIENT_AUTH = "NONE";
+
+
     @JsonProperty("enabled")
     protected final boolean enabled;
 
+    @JsonProperty(value = "use_openssl", defaultValue = "true")
+    protected final boolean useOpenSsl;
+
+    @JsonProperty(value = "handshake_timeout_sec", defaultValue = "10")
+    protected final long handshakeTimeoutInSeconds;
+
+    protected String clientAuth;
+
     @JsonProperty("keystore")
     protected final KeyStoreConfiguration keystore;
 
@@ -38,20 +57,21 @@
 
     public SslConfigurationImpl()
     {
-        this(false, null, null);
+        this(builder());
     }
 
-    public SslConfigurationImpl(boolean enabled,
-                                KeyStoreConfiguration keystore,
-                                KeyStoreConfiguration truststore)
+    protected SslConfigurationImpl(Builder builder)
     {
-        this.enabled = enabled;
-        this.keystore = keystore;
-        this.truststore = truststore;
+        enabled = builder.enabled;
+        useOpenSsl = builder.useOpenSsl;
+        handshakeTimeoutInSeconds = builder.handshakeTimeoutInSeconds;
+        setClientAuth(builder.clientAuth);
+        keystore = builder.keystore;
+        truststore = builder.truststore;
     }
 
     /**
-     * @return {@code true} if SSL is enabled, {@code false} otherwise
+     * {@inheritDoc}
      */
     @Override
     @JsonProperty("enabled")
@@ -61,17 +81,54 @@
     }
 
     /**
-     * @return {@code true} if the keystore is configured, and the {@link KeyStoreConfiguration#path()} and
-     * {@link KeyStoreConfiguration#password()} parameters are provided
+     * {@inheritDoc}
      */
     @Override
-    public boolean isKeystoreConfigured()
+    @JsonProperty(value = "use_openssl", defaultValue = "true")
+    public boolean preferOpenSSL()
     {
-        return keystore != null && keystore.isConfigured();
+        return useOpenSsl;
     }
 
     /**
-     * @return the configuration for the keystore
+     * {@inheritDoc}
+     */
+    @Override
+    @JsonProperty(value = "handshake_timeout_sec", defaultValue = "10")
+    public long handshakeTimeoutInSeconds()
+    {
+        return handshakeTimeoutInSeconds;
+    }
+
+    @Override
+    @JsonProperty(value = "client_auth", defaultValue = "NONE")
+    public String clientAuth()
+    {
+        return clientAuth;
+    }
+
+    @JsonProperty(value = "client_auth", defaultValue = "NONE")
+    public void setClientAuth(String clientAuth)
+    {
+        this.clientAuth = clientAuth;
+        try
+        {
+            // forces a validation of the input
+            this.clientAuth = ClientAuth.valueOf(clientAuth).name();
+        }
+        catch (IllegalArgumentException exception)
+        {
+            String errorMessage = String.format("Invalid client_auth configuration=\"%s\", valid values are (%s)",
+                                                clientAuth,
+                                                Arrays.stream(ClientAuth.values())
+                                                      .map(Enum::name)
+                                                      .collect(Collectors.joining(",")));
+            throw new IllegalArgumentException(errorMessage);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
      */
     @Override
     @JsonProperty("keystore")
@@ -81,17 +138,16 @@
     }
 
     /**
-     * @return {@code true} if the truststore is configured, and the {@link KeyStoreConfiguration#path()} and
-     * {@link KeyStoreConfiguration#password()} parameters are provided
+     * {@inheritDoc}
      */
     @Override
-    public boolean isTruststoreConfigured()
+    public boolean isTrustStoreConfigured()
     {
         return truststore != null && truststore.isConfigured();
     }
 
     /**
-     * @return the configuration for the truststore
+     * {@inheritDoc}
      */
     @Override
     @JsonProperty("truststore")
@@ -99,4 +155,115 @@
     {
         return truststore;
     }
+
+    public static Builder builder()
+    {
+        return new Builder();
+    }
+
+    /**
+     * {@code SslConfigurationImpl} builder static inner class.
+     */
+    public static class Builder implements DataObjectBuilder<Builder, SslConfigurationImpl>
+    {
+        private boolean enabled = DEFAULT_SSL_ENABLED;
+        private boolean useOpenSsl = DEFAULT_USE_OPEN_SSL;
+        private long handshakeTimeoutInSeconds = DEFAULT_HANDSHAKE_TIMEOUT_SECONDS;
+        private String clientAuth = DEFAULT_CLIENT_AUTH;
+        private KeyStoreConfiguration keystore = null;
+        private KeyStoreConfiguration truststore = null;
+
+        protected Builder()
+        {
+        }
+
+        @Override
+        public Builder self()
+        {
+            return this;
+        }
+
+        /**
+         * Sets the {@code enabled} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param enabled the {@code enabled} to set
+         * @return a reference to this Builder
+         */
+        public Builder enabled(boolean enabled)
+        {
+            this.enabled = enabled;
+            return this;
+        }
+
+        /**
+         * Sets the {@code useOpenSsl} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param useOpenSsl the {@code useOpenSsl} to set
+         * @return a reference to this Builder
+         */
+        public Builder useOpenSsl(boolean useOpenSsl)
+        {
+            this.useOpenSsl = useOpenSsl;
+            return this;
+        }
+
+        /**
+         * Sets the {@code handshakeTimeoutInSeconds} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param handshakeTimeoutInSeconds the {@code handshakeTimeoutInSeconds} to set
+         * @return a reference to this Builder
+         */
+        public Builder handshakeTimeoutInSeconds(long handshakeTimeoutInSeconds)
+        {
+            this.handshakeTimeoutInSeconds = handshakeTimeoutInSeconds;
+            return this;
+        }
+
+        /**
+         * Sets the {@code clientAuth} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param clientAuth the {@code clientAuth} to set
+         * @return a reference to this Builder
+         */
+        public Builder clientAuth(String clientAuth)
+        {
+            this.clientAuth = clientAuth;
+            return this;
+        }
+
+        /**
+         * Sets the {@code keystore} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param keystore the {@code keystore} to set
+         * @return a reference to this Builder
+         */
+        public Builder keystore(KeyStoreConfiguration keystore)
+        {
+            this.keystore = keystore;
+            return this;
+        }
+
+        /**
+         * Sets the {@code truststore} and returns a reference to this Builder enabling method chaining.
+         *
+         * @param truststore the {@code truststore} to set
+         * @return a reference to this Builder
+         */
+        public Builder truststore(KeyStoreConfiguration truststore)
+        {
+            this.truststore = truststore;
+            return this;
+        }
+
+        /**
+         * Returns a {@code SslConfigurationImpl} built from the parameters previously set.
+         *
+         * @return a {@code SslConfigurationImpl} built with parameters of this {@code SslConfigurationImpl.Builder}
+         */
+        @Override
+        public SslConfigurationImpl build()
+        {
+            return new SslConfigurationImpl(this);
+        }
+    }
 }
diff --git a/src/main/java/org/apache/cassandra/sidecar/config/yaml/TrafficShapingConfigurationImpl.java b/src/main/java/org/apache/cassandra/sidecar/config/yaml/TrafficShapingConfigurationImpl.java
new file mode 100644
index 0000000..f14573b
--- /dev/null
+++ b/src/main/java/org/apache/cassandra/sidecar/config/yaml/TrafficShapingConfigurationImpl.java
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.config.yaml;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.netty.handler.traffic.AbstractTrafficShapingHandler;
+import io.netty.handler.traffic.GlobalTrafficShapingHandler;
+import org.apache.cassandra.sidecar.config.TrafficShapingConfiguration;
+
+/**
+ * Reads the configuration for the global traffic shaping options from a YAML file. These TCP server options enable
+ * configuration of bandwidth limiting. Both inbound and outbound bandwidth can be limited through these options.
+ */
+public class TrafficShapingConfigurationImpl implements TrafficShapingConfiguration
+{
+    /**
+     * Default inbound bandwidth limit in bytes/sec = 0 (0 implies unthrottled)
+     */
+    public static final long DEFAULT_INBOUND_GLOBAL_BANDWIDTH_LIMIT = 0;
+
+    /**
+     * Default outbound bandwidth limit in bytes/sec = 0 (0 implies unthrottled)
+     */
+    public static final long DEFAULT_OUTBOUND_GLOBAL_BANDWIDTH_LIMIT = 0;
+
+    /**
+     * Default peak outbound bandwidth limit. Defaults to 400 megabytes/sec
+     * See {@link GlobalTrafficShapingHandler#maxGlobalWriteSize}
+     */
+    public static final long DEFAULT_PEAK_OUTBOUND_GLOBAL_BANDWIDTH_LIMIT = 400L * 1024L * 1024L;
+
+    /**
+     * Default max delay in case of traffic shaping
+     * (during which no communication will occur).
+     * Shall be less than TIMEOUT. Here half of "standard" 30s.
+     * See {@link AbstractTrafficShapingHandler#DEFAULT_MAX_TIME}
+     */
+    public static final long DEFAULT_MAX_DELAY_TIME = 15000L;
+
+    /**
+     * Default delay between two checks: 1s (1000ms)
+     * See {@link AbstractTrafficShapingHandler#DEFAULT_CHECK_INTERVAL}
+     */
+    public static final long DEFAULT_CHECK_INTERVAL = 1000L;
+
+    /**
+     * Default inbound bandwidth limit in bytes/sec for ingress files = 0 (0 implies unthrottled)
+     */
+    public static final long DEFAULT_INBOUND_FILE_GLOBAL_BANDWIDTH_LIMIT = 0;
+
+    @JsonProperty(value = "inbound_global_bandwidth_bps", defaultValue = "0")
+    protected final long inboundGlobalBandwidthBytesPerSecond;
+
+    @JsonProperty(value = "outbound_global_bandwidth_bps", defaultValue = "0")
+    protected final long outboundGlobalBandwidthBytesPerSecond;
+
+    @JsonProperty(value = "peak_outbound_global_bandwidth_bps",
+    defaultValue = DEFAULT_PEAK_OUTBOUND_GLOBAL_BANDWIDTH_LIMIT + "")
+    protected final long peakOutboundGlobalBandwidthBytesPerSecond;
+
+    @JsonProperty(value = "max_delay_to_wait_millis", defaultValue = DEFAULT_MAX_DELAY_TIME + "")
+    protected final long maxDelayToWaitMillis;
+
+    @JsonProperty(value = "check_interval_for_stats_millis", defaultValue = DEFAULT_CHECK_INTERVAL + "")
+    protected final long checkIntervalForStatsMillis;
+
+    @JsonProperty(value = "inbound_global_file_bandwidth_bps", defaultValue = "0")
+    protected final long inboundGlobalFileBandwidthBytesPerSecond;
+
+    public TrafficShapingConfigurationImpl()
+    {
+        this(DEFAULT_INBOUND_GLOBAL_BANDWIDTH_LIMIT,
+             DEFAULT_OUTBOUND_GLOBAL_BANDWIDTH_LIMIT,
+             DEFAULT_PEAK_OUTBOUND_GLOBAL_BANDWIDTH_LIMIT,
+             DEFAULT_MAX_DELAY_TIME,
+             DEFAULT_CHECK_INTERVAL,
+             DEFAULT_INBOUND_FILE_GLOBAL_BANDWIDTH_LIMIT
+        );
+    }
+
+    public TrafficShapingConfigurationImpl(long inboundGlobalBandwidthBytesPerSecond,
+                                           long outboundGlobalBandwidthBytesPerSecond,
+                                           long peakOutboundGlobalBandwidthBytesPerSecond,
+                                           long maxDelayToWaitMillis,
+                                           long checkIntervalForStatsMillis,
+                                           long inboundGlobalFileBandwidthBytesPerSecond)
+    {
+        this.inboundGlobalBandwidthBytesPerSecond = inboundGlobalBandwidthBytesPerSecond;
+        this.outboundGlobalBandwidthBytesPerSecond = outboundGlobalBandwidthBytesPerSecond;
+        this.peakOutboundGlobalBandwidthBytesPerSecond = peakOutboundGlobalBandwidthBytesPerSecond;
+        this.maxDelayToWaitMillis = maxDelayToWaitMillis;
+        this.checkIntervalForStatsMillis = checkIntervalForStatsMillis;
+        this.inboundGlobalFileBandwidthBytesPerSecond = inboundGlobalFileBandwidthBytesPerSecond;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    @JsonProperty(value = "inbound_global_bandwidth_bps", defaultValue = "0")
+    public long inboundGlobalBandwidthBytesPerSecond()
+    {
+        return inboundGlobalBandwidthBytesPerSecond;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    @JsonProperty(value = "outbound_global_bandwidth_bps", defaultValue = "0")
+    public long outboundGlobalBandwidthBytesPerSecond()
+    {
+        return outboundGlobalBandwidthBytesPerSecond;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    @JsonProperty(value = "peak_outbound_global_bandwidth_bps",
+    defaultValue = DEFAULT_PEAK_OUTBOUND_GLOBAL_BANDWIDTH_LIMIT + "")
+    public long peakOutboundGlobalBandwidthBytesPerSecond()
+    {
+        return peakOutboundGlobalBandwidthBytesPerSecond;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    @JsonProperty(value = "max_delay_to_wait_millis", defaultValue = DEFAULT_MAX_DELAY_TIME + "")
+    public long maxDelayToWaitMillis()
+    {
+        return maxDelayToWaitMillis;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    @JsonProperty(value = "check_interval_for_stats_millis", defaultValue = DEFAULT_CHECK_INTERVAL + "")
+    public long checkIntervalForStatsMillis()
+    {
+        return checkIntervalForStatsMillis;
+    }
+
+    @Override
+    @JsonProperty(value = "inbound_global_file_bandwidth_bps", defaultValue = "0")
+    public long inboundGlobalFileBandwidthBytesPerSecond()
+    {
+        return inboundGlobalFileBandwidthBytesPerSecond;
+    }
+}
diff --git a/src/main/java/org/apache/cassandra/sidecar/routes/CassandraHealthHandler.java b/src/main/java/org/apache/cassandra/sidecar/routes/CassandraHealthHandler.java
index 0366ea8..4713535 100644
--- a/src/main/java/org/apache/cassandra/sidecar/routes/CassandraHealthHandler.java
+++ b/src/main/java/org/apache/cassandra/sidecar/routes/CassandraHealthHandler.java
@@ -26,13 +26,13 @@
 import io.vertx.core.json.Json;
 import io.vertx.core.net.SocketAddress;
 import io.vertx.ext.web.RoutingContext;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
 import org.apache.cassandra.sidecar.utils.CassandraInputValidator;
 import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
 
-import static org.apache.cassandra.sidecar.MainModule.NOT_OK_STATUS;
-import static org.apache.cassandra.sidecar.MainModule.OK_STATUS;
+import static org.apache.cassandra.sidecar.server.MainModule.NOT_OK_STATUS;
+import static org.apache.cassandra.sidecar.server.MainModule.OK_STATUS;
 
 /**
  * Provides a simple REST endpoint to determine if a Cassandra node is available
diff --git a/src/main/java/org/apache/cassandra/sidecar/routes/GossipInfoHandler.java b/src/main/java/org/apache/cassandra/sidecar/routes/GossipInfoHandler.java
index e0f90c7..2ae3912 100644
--- a/src/main/java/org/apache/cassandra/sidecar/routes/GossipInfoHandler.java
+++ b/src/main/java/org/apache/cassandra/sidecar/routes/GossipInfoHandler.java
@@ -24,7 +24,7 @@
 import io.vertx.core.http.HttpServerRequest;
 import io.vertx.core.net.SocketAddress;
 import io.vertx.ext.web.RoutingContext;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.common.ClusterMembershipOperations;
 import org.apache.cassandra.sidecar.common.data.GossipInfoResponse;
 import org.apache.cassandra.sidecar.common.utils.GossipInfoParser;
diff --git a/src/main/java/org/apache/cassandra/sidecar/routes/RingHandler.java b/src/main/java/org/apache/cassandra/sidecar/routes/RingHandler.java
index f5a78b8..8ed3900 100644
--- a/src/main/java/org/apache/cassandra/sidecar/routes/RingHandler.java
+++ b/src/main/java/org/apache/cassandra/sidecar/routes/RingHandler.java
@@ -28,7 +28,7 @@
 import io.vertx.core.http.HttpServerRequest;
 import io.vertx.core.net.SocketAddress;
 import io.vertx.ext.web.RoutingContext;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.common.StorageOperations;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
 import org.apache.cassandra.sidecar.data.RingRequest;
diff --git a/src/main/java/org/apache/cassandra/sidecar/routes/SchemaHandler.java b/src/main/java/org/apache/cassandra/sidecar/routes/SchemaHandler.java
index b6b7be3..808003e 100644
--- a/src/main/java/org/apache/cassandra/sidecar/routes/SchemaHandler.java
+++ b/src/main/java/org/apache/cassandra/sidecar/routes/SchemaHandler.java
@@ -26,7 +26,7 @@
 import io.vertx.core.http.HttpServerRequest;
 import io.vertx.core.net.SocketAddress;
 import io.vertx.ext.web.RoutingContext;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.common.data.SchemaResponse;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
 import org.apache.cassandra.sidecar.data.SchemaRequest;
diff --git a/src/main/java/org/apache/cassandra/sidecar/routes/SnapshotsHandler.java b/src/main/java/org/apache/cassandra/sidecar/routes/SnapshotsHandler.java
index bf9f106..f6669e4 100644
--- a/src/main/java/org/apache/cassandra/sidecar/routes/SnapshotsHandler.java
+++ b/src/main/java/org/apache/cassandra/sidecar/routes/SnapshotsHandler.java
@@ -35,7 +35,7 @@
 import io.vertx.core.net.SocketAddress;
 import io.vertx.ext.web.RoutingContext;
 import io.vertx.ext.web.handler.HttpException;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.common.StorageOperations;
 import org.apache.cassandra.sidecar.common.data.ListSnapshotFilesResponse;
 import org.apache.cassandra.sidecar.common.exceptions.NodeBootstrappingException;
diff --git a/src/main/java/org/apache/cassandra/sidecar/routes/TokenRangeReplicaMapHandler.java b/src/main/java/org/apache/cassandra/sidecar/routes/TokenRangeReplicaMapHandler.java
index f4b824e..aea5ffc 100644
--- a/src/main/java/org/apache/cassandra/sidecar/routes/TokenRangeReplicaMapHandler.java
+++ b/src/main/java/org/apache/cassandra/sidecar/routes/TokenRangeReplicaMapHandler.java
@@ -27,7 +27,7 @@
 import io.vertx.core.http.HttpServerRequest;
 import io.vertx.core.net.SocketAddress;
 import io.vertx.ext.web.RoutingContext;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.common.StorageOperations;
 import org.apache.cassandra.sidecar.common.data.TokenRangeReplicasRequest;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
diff --git a/src/main/java/org/apache/cassandra/sidecar/routes/cassandra/NodeSettingsHandler.java b/src/main/java/org/apache/cassandra/sidecar/routes/cassandra/NodeSettingsHandler.java
index 57c897c..bc72cc5 100644
--- a/src/main/java/org/apache/cassandra/sidecar/routes/cassandra/NodeSettingsHandler.java
+++ b/src/main/java/org/apache/cassandra/sidecar/routes/cassandra/NodeSettingsHandler.java
@@ -24,7 +24,7 @@
 import io.vertx.core.http.HttpServerRequest;
 import io.vertx.core.net.SocketAddress;
 import io.vertx.ext.web.RoutingContext;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.common.NodeSettings;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
 import org.apache.cassandra.sidecar.routes.AbstractHandler;
diff --git a/src/main/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableImportHandler.java b/src/main/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableImportHandler.java
index bba6c67..ef8b366 100644
--- a/src/main/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableImportHandler.java
+++ b/src/main/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableImportHandler.java
@@ -28,7 +28,7 @@
 import io.vertx.core.net.SocketAddress;
 import io.vertx.ext.web.RoutingContext;
 import io.vertx.ext.web.handler.HttpException;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.common.TableOperations;
 import org.apache.cassandra.sidecar.common.data.SSTableImportResponse;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
diff --git a/src/main/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableUploadHandler.java b/src/main/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableUploadHandler.java
index b81f622..845dd4c 100644
--- a/src/main/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableUploadHandler.java
+++ b/src/main/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableUploadHandler.java
@@ -31,7 +31,7 @@
 import io.vertx.core.http.HttpServerRequest;
 import io.vertx.core.net.SocketAddress;
 import io.vertx.ext.web.RoutingContext;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.common.data.SSTableUploadResponse;
 import org.apache.cassandra.sidecar.concurrent.ConcurrencyLimiter;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
diff --git a/src/main/java/org/apache/cassandra/sidecar/server/HttpServerOptionsProvider.java b/src/main/java/org/apache/cassandra/sidecar/server/HttpServerOptionsProvider.java
new file mode 100644
index 0000000..6ee3e65
--- /dev/null
+++ b/src/main/java/org/apache/cassandra/sidecar/server/HttpServerOptionsProvider.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.server;
+
+import java.util.function.Function;
+
+import org.apache.commons.lang3.SystemUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.inject.Singleton;
+import io.vertx.core.http.ClientAuth;
+import io.vertx.core.http.HttpServerOptions;
+import io.vertx.core.net.OpenSSLEngineOptions;
+import io.vertx.core.net.SSLOptions;
+import io.vertx.core.net.TrafficShapingOptions;
+import org.apache.cassandra.sidecar.config.ServiceConfiguration;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.config.SslConfiguration;
+import org.apache.cassandra.sidecar.config.TrafficShapingConfiguration;
+import org.apache.cassandra.sidecar.utils.SslUtils;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+/**
+ * A provider that takes the {@link SidecarConfiguration} and builds {@link HttpServerOptions} from the configured
+ * values
+ */
+@Singleton
+public class HttpServerOptionsProvider implements Function<SidecarConfiguration, HttpServerOptions>
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(HttpServerOptionsProvider.class);
+
+    /**
+     * @return the {@link HttpServerOptions} built from the provided {@link SidecarConfiguration}
+     */
+    @Override
+    public HttpServerOptions apply(SidecarConfiguration configuration)
+    {
+        HttpServerOptions options = new HttpServerOptions().setLogActivity(true);
+        ServiceConfiguration serviceConf = configuration.serviceConfiguration();
+        options.setIdleTimeoutUnit(MILLISECONDS)
+               .setIdleTimeout(serviceConf.requestIdleTimeoutMillis())
+               .setTcpKeepAlive(serviceConf.tcpKeepAlive())
+               .setAcceptBacklog(serviceConf.acceptBacklog());
+
+        if (SystemUtils.IS_OS_LINUX)
+        {
+            options.setTcpFastOpen(true);
+            options.setTcpCork(true);
+        }
+
+        SslConfiguration ssl = configuration.sslConfiguration();
+        if (ssl != null && ssl.enabled())
+        {
+            options.setClientAuth(ClientAuth.valueOf(ssl.clientAuth()))
+                   .setSsl(true);
+
+            if (ssl.preferOpenSSL() && OpenSSLEngineOptions.isAvailable())
+            {
+                LOGGER.info("Using OpenSSL for encryption");
+                options.setSslEngineOptions(new OpenSSLEngineOptions().setSessionCacheEnabled(true));
+            }
+            else
+            {
+                LOGGER.warn("OpenSSL not enabled, using JDK for TLS");
+            }
+
+            configureSSLOptions(options.getSslOptions(), ssl, 0);
+        }
+
+        options.setTrafficShapingOptions(buildTrafficShapingOptions(serviceConf.trafficShapingConfiguration()));
+        return options;
+    }
+
+    /**
+     * Configures the SSL options for the server
+     *
+     * @param options   the SSL options
+     * @param ssl       the SSL configuration
+     * @param timestamp a timestamp for the keystore file for when the file was last changed, or 0 for the startup value
+     */
+    protected void configureSSLOptions(SSLOptions options, SslConfiguration ssl, long timestamp)
+    {
+        options.setSslHandshakeTimeout(ssl.handshakeTimeoutInSeconds())
+               .setSslHandshakeTimeoutUnit(SECONDS);
+
+        configureKeyStore(options, ssl, timestamp);
+        configureTrustStore(options, ssl);
+    }
+
+    /**
+     * Configures the key store
+     *
+     * @param options   the SSL options
+     * @param ssl       the SSL configuration
+     * @param timestamp a timestamp for the keystore file for when the file was last changed, or 0 for the startup value
+     */
+    protected void configureKeyStore(SSLOptions options, SslConfiguration ssl, long timestamp)
+    {
+        SslUtils.setKeyStoreConfiguration(options, ssl.keystore(), timestamp);
+    }
+
+    /**
+     * Configures the trust store if provided
+     *
+     * @param options the SSL options
+     * @param ssl     the SSL configuration
+     */
+    protected void configureTrustStore(SSLOptions options, SslConfiguration ssl)
+    {
+        if (ssl.isTrustStoreConfigured())
+        {
+            SslUtils.setTrustStoreConfiguration(options, ssl.truststore());
+        }
+    }
+
+    /**
+     * Returns the built {@link TrafficShapingOptions} that are going to be applied to the server.
+     *
+     * @param trafficShapingConfig the configuration for the traffic shaping options.
+     * @return the built {@link TrafficShapingOptions} from the {@link TrafficShapingConfiguration}
+     */
+    protected TrafficShapingOptions buildTrafficShapingOptions(TrafficShapingConfiguration trafficShapingConfig)
+    {
+        return new TrafficShapingOptions()
+               .setInboundGlobalBandwidth(trafficShapingConfig.inboundGlobalBandwidthBytesPerSecond())
+               .setOutboundGlobalBandwidth(trafficShapingConfig.outboundGlobalBandwidthBytesPerSecond())
+               .setPeakOutboundGlobalBandwidth(trafficShapingConfig.peakOutboundGlobalBandwidthBytesPerSecond())
+               .setCheckIntervalForStats(trafficShapingConfig.checkIntervalForStatsMillis())
+               .setCheckIntervalForStatsTimeUnit(MILLISECONDS)
+               .setMaxDelayToWait(trafficShapingConfig.maxDelayToWaitMillis())
+               .setMaxDelayToWaitUnit(MILLISECONDS);
+    }
+}
diff --git a/src/main/java/org/apache/cassandra/sidecar/MainModule.java b/src/main/java/org/apache/cassandra/sidecar/server/MainModule.java
similarity index 84%
rename from src/main/java/org/apache/cassandra/sidecar/MainModule.java
rename to src/main/java/org/apache/cassandra/sidecar/server/MainModule.java
index 924ac83..ddabae3 100644
--- a/src/main/java/org/apache/cassandra/sidecar/MainModule.java
+++ b/src/main/java/org/apache/cassandra/sidecar/server/MainModule.java
@@ -16,13 +16,13 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.sidecar;
+package org.apache.cassandra.sidecar.server;
 
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 import com.google.common.util.concurrent.SidecarRateLimiter;
@@ -30,13 +30,11 @@
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
+import com.google.inject.name.Named;
 import io.netty.handler.codec.http.HttpResponseStatus;
 import io.vertx.core.Vertx;
 import io.vertx.core.VertxOptions;
 import io.vertx.core.http.HttpMethod;
-import io.vertx.core.http.HttpServer;
-import io.vertx.core.http.HttpServerOptions;
-import io.vertx.core.net.JksOptions;
 import io.vertx.ext.dropwizard.DropwizardMetricsOptions;
 import io.vertx.ext.web.Router;
 import io.vertx.ext.web.handler.ErrorHandler;
@@ -44,14 +42,13 @@
 import io.vertx.ext.web.handler.StaticHandler;
 import io.vertx.ext.web.handler.TimeoutHandler;
 import org.apache.cassandra.sidecar.adapters.base.CassandraFactory;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.cluster.InstancesConfig;
 import org.apache.cassandra.sidecar.cluster.InstancesConfigImpl;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadataImpl;
 import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
 import org.apache.cassandra.sidecar.common.CQLSessionProvider;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
-import org.apache.cassandra.sidecar.common.CassandraVersionProvider;
 import org.apache.cassandra.sidecar.common.JmxClient;
 import org.apache.cassandra.sidecar.common.dns.DnsResolver;
 import org.apache.cassandra.sidecar.common.utils.SidecarVersionProvider;
@@ -60,7 +57,6 @@
 import org.apache.cassandra.sidecar.config.JmxConfiguration;
 import org.apache.cassandra.sidecar.config.ServiceConfiguration;
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
-import org.apache.cassandra.sidecar.config.SslConfiguration;
 import org.apache.cassandra.sidecar.config.yaml.SidecarConfigurationImpl;
 import org.apache.cassandra.sidecar.logging.SidecarLoggerHandler;
 import org.apache.cassandra.sidecar.routes.CassandraHealthHandler;
@@ -78,6 +74,7 @@
 import org.apache.cassandra.sidecar.routes.sstableuploads.SSTableImportHandler;
 import org.apache.cassandra.sidecar.routes.sstableuploads.SSTableUploadHandler;
 import org.apache.cassandra.sidecar.stats.SidecarStats;
+import org.apache.cassandra.sidecar.utils.CassandraVersionProvider;
 import org.apache.cassandra.sidecar.utils.ChecksumVerifier;
 import org.apache.cassandra.sidecar.utils.MD5ChecksumVerifier;
 import org.apache.cassandra.sidecar.utils.TimeProvider;
@@ -90,6 +87,26 @@
     public static final Map<String, String> OK_STATUS = Collections.singletonMap("status", "OK");
     public static final Map<String, String> NOT_OK_STATUS = Collections.singletonMap("status", "NOT_OK");
 
+    protected final Path confPath;
+
+    /**
+     * Constructs the Guice main module to run Cassandra Sidecar
+     */
+    public MainModule()
+    {
+        confPath = null;
+    }
+
+    /**
+     * Constructs the Guice main module with the configured yaml {@code confPath} to run Cassandra Sidecar
+     *
+     * @param confPath the path to the yaml configuration file
+     */
+    public MainModule(Path confPath)
+    {
+        this.confPath = confPath;
+    }
+
     @Provides
     @Singleton
     public Vertx vertx()
@@ -102,34 +119,6 @@
 
     @Provides
     @Singleton
-    public HttpServer vertxServer(Vertx vertx, SidecarConfiguration conf, Router router)
-    {
-        HttpServerOptions options = new HttpServerOptions().setLogActivity(true);
-        options.setIdleTimeoutUnit(TimeUnit.MILLISECONDS)
-               .setIdleTimeout(conf.serviceConfiguration().requestIdleTimeoutMillis());
-
-        SslConfiguration ssl = conf.sslConfiguration();
-        if (ssl != null && ssl.enabled())
-        {
-            options.setKeyStoreOptions(new JksOptions()
-                                       .setPath(ssl.keystore().path())
-                                       .setPassword(ssl.keystore().password()))
-                   .setSsl(ssl.enabled());
-
-            if (ssl.isTruststoreConfigured())
-            {
-                options.setTrustStoreOptions(new JksOptions()
-                                             .setPath(ssl.truststore().path())
-                                             .setPassword(ssl.truststore().password()));
-            }
-        }
-
-        return vertx.createHttpServer(options)
-                    .requestHandler(router);
-    }
-
-    @Provides
-    @Singleton
     public Router vertxRouter(Vertx vertx,
                               ServiceConfiguration conf,
                               CassandraHealthHandler cassandraHealthHandler,
@@ -245,7 +234,10 @@
     @Singleton
     public SidecarConfiguration sidecarConfiguration() throws IOException
     {
-        final String confPath = System.getProperty("sidecar.config", "file://./conf/config.yaml");
+        if (confPath == null)
+        {
+            throw new NullPointerException("the YAML configuration path for Sidecar has not been defined.");
+        }
         return SidecarConfigurationImpl.readYamlConfiguration(confPath);
     }
 
@@ -265,7 +257,8 @@
 
     @Provides
     @Singleton
-    public InstancesConfig instancesConfig(SidecarConfiguration configuration,
+    public InstancesConfig instancesConfig(Vertx vertx,
+                                           SidecarConfiguration configuration,
                                            CassandraVersionProvider cassandraVersionProvider,
                                            SidecarVersionProvider sidecarVersionProvider,
                                            DnsResolver dnsResolver)
@@ -277,7 +270,8 @@
                      .stream()
                      .map(cassandraInstance -> {
                          JmxConfiguration jmxConfiguration = configuration.serviceConfiguration().jmxConfiguration();
-                         return buildInstanceMetadata(cassandraInstance,
+                         return buildInstanceMetadata(vertx,
+                                                      cassandraInstance,
                                                       cassandraVersionProvider,
                                                       healthCheckFrequencyMillis,
                                                       sidecarVersionProvider.sidecarVersion(),
@@ -308,6 +302,15 @@
 
     @Provides
     @Singleton
+    @Named("IngressFileRateLimiter")
+    public SidecarRateLimiter ingressFileRateLimiter(ServiceConfiguration serviceConfiguration)
+    {
+        return SidecarRateLimiter.create(serviceConfiguration.trafficShapingConfiguration()
+                                                             .inboundGlobalFileBandwidthBytesPerSecond());
+    }
+
+    @Provides
+    @Singleton
     public LoggerHandler loggerHandler()
     {
         return SidecarLoggerHandler.create(LoggerHandler.create());
@@ -359,14 +362,16 @@
      * Builds the {@link InstanceMetadata} from the {@link InstanceConfiguration},
      * a provided {@code  versionProvider}, and {@code healthCheckFrequencyMillis}.
      *
+     * @param vertx                      the vertx instance
      * @param cassandraInstance          the cassandra instance configuration
      * @param versionProvider            a Cassandra version provider
      * @param healthCheckFrequencyMillis the health check frequency configuration in milliseconds
      * @param sidecarVersion             the version of the Sidecar from the current binary
-     * @param jmxConfiguration
+     * @param jmxConfiguration           the configuration for the JMX Client
      * @return the build instance metadata object
      */
-    private static InstanceMetadata buildInstanceMetadata(InstanceConfiguration cassandraInstance,
+    private static InstanceMetadata buildInstanceMetadata(Vertx vertx,
+                                                          InstanceConfiguration cassandraInstance,
                                                           CassandraVersionProvider versionProvider,
                                                           int healthCheckFrequencyMillis,
                                                           String sidecarVersion,
@@ -376,14 +381,18 @@
         int port = cassandraInstance.port();
 
         CQLSessionProvider session = new CQLSessionProvider(host, port, healthCheckFrequencyMillis);
-        JmxClient jmxClient = new JmxClient(cassandraInstance.jmxHost(),
-                                            cassandraInstance.jmxPort(),
-                                            cassandraInstance.jmxRole(),
-                                            cassandraInstance.jmxRolePassword(),
-                                            cassandraInstance.jmxSslEnabled(),
-                                            jmxConfiguration.maxRetries(),
-                                            jmxConfiguration.retryDelayMillis());
-        CassandraAdapterDelegate delegate = new CassandraAdapterDelegate(versionProvider,
+        JmxClient jmxClient = JmxClient.builder()
+                                       .host(cassandraInstance.jmxHost())
+                                       .port(cassandraInstance.jmxPort())
+                                       .role(cassandraInstance.jmxRole())
+                                       .password(cassandraInstance.jmxRolePassword())
+                                       .enableSsl(cassandraInstance.jmxSslEnabled())
+                                       .connectionMaxRetries(jmxConfiguration.maxRetries())
+                                       .connectionRetryDelayMillis(jmxConfiguration.retryDelayMillis())
+                                       .build();
+        CassandraAdapterDelegate delegate = new CassandraAdapterDelegate(vertx,
+                                                                         cassandraInstance.id(),
+                                                                         versionProvider,
                                                                          session,
                                                                          jmxClient,
                                                                          sidecarVersion);
diff --git a/src/main/java/org/apache/cassandra/sidecar/server/Server.java b/src/main/java/org/apache/cassandra/sidecar/server/Server.java
new file mode 100644
index 0000000..b3fd01d
--- /dev/null
+++ b/src/main/java/org/apache/cassandra/sidecar/server/Server.java
@@ -0,0 +1,330 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.server;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.vertx.core.CompositeFuture;
+import io.vertx.core.DeploymentOptions;
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.core.Vertx;
+import io.vertx.core.eventbus.Message;
+import io.vertx.core.eventbus.MessageConsumer;
+import io.vertx.core.http.HttpServerOptions;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import io.vertx.core.net.SSLOptions;
+import io.vertx.ext.web.Router;
+import org.apache.cassandra.sidecar.cluster.InstancesConfig;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.utils.Preconditions;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.config.SslConfiguration;
+import org.apache.cassandra.sidecar.tasks.HealthCheckPeriodicTask;
+import org.apache.cassandra.sidecar.tasks.KeyStoreCheckPeriodicTask;
+import org.apache.cassandra.sidecar.tasks.PeriodicTaskExecutor;
+import org.apache.cassandra.sidecar.utils.SslUtils;
+import org.jetbrains.annotations.VisibleForTesting;
+
+import static org.apache.cassandra.sidecar.server.SidecarServerEvents.ON_ALL_CASSANDRA_CQL_READY;
+import static org.apache.cassandra.sidecar.server.SidecarServerEvents.ON_CASSANDRA_CQL_READY;
+
+/**
+ * The Sidecar {@link Server} class that manages the start and stop lifecycle of the service
+ */
+@Singleton
+public class Server
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(Server.class);
+    protected final Vertx vertx;
+    protected final ExecutorPools executorPools;
+    protected final SidecarConfiguration sidecarConfiguration;
+    protected final InstancesConfig instancesConfig;
+    protected final Router router;
+    protected final PeriodicTaskExecutor periodicTaskExecutor;
+    protected final HttpServerOptionsProvider optionsProvider;
+    protected final List<ServerVerticle> deployedServerVerticles = new CopyOnWriteArrayList<>();
+    // Keeps track of all the Cassandra instance identifiers where CQL is ready
+    private final Set<Integer> cqlReadyInstanceIds = Collections.synchronizedSet(new HashSet<>());
+
+    @Inject
+    public Server(Vertx vertx,
+                  SidecarConfiguration sidecarConfiguration,
+                  Router router,
+                  InstancesConfig instancesConfig,
+                  ExecutorPools executorPools,
+                  PeriodicTaskExecutor periodicTaskExecutor,
+                  HttpServerOptionsProvider optionsProvider)
+    {
+        this.vertx = vertx;
+        this.executorPools = executorPools;
+        this.sidecarConfiguration = sidecarConfiguration;
+        this.instancesConfig = instancesConfig;
+        this.router = router;
+        this.periodicTaskExecutor = periodicTaskExecutor;
+        this.optionsProvider = optionsProvider;
+    }
+
+    /**
+     * Deploys the {@link ServerVerticle verticles} to {@link Vertx}.
+     *
+     * @return a future completed with the result
+     */
+    public Future<String> start()
+    {
+        banner(System.out);
+        validate();
+
+        LOGGER.info("Starting Cassandra Sidecar");
+        int serverVerticleCount = sidecarConfiguration.serviceConfiguration().serverVerticleInstances();
+        Preconditions.checkArgument(serverVerticleCount > 0,
+                                    "Server verticle count can not be less than 1");
+        LOGGER.debug("Deploying {} verticles to vertx", serverVerticleCount);
+        DeploymentOptions deploymentOptions = new DeploymentOptions().setInstances(serverVerticleCount);
+        HttpServerOptions options = optionsProvider.apply(sidecarConfiguration);
+        return vertx.deployVerticle(() -> {
+                        ServerVerticle serverVerticle = new ServerVerticle(sidecarConfiguration, router, options);
+                        deployedServerVerticles.add(serverVerticle);
+                        return serverVerticle;
+                    }, deploymentOptions)
+                    .compose(this::scheduleInternalPeriodicTasks)
+                    .compose(this::notifyServerStart);
+    }
+
+    /**
+     * Undeploy a verticle deployment, stopping all the {@link ServerVerticle verticles}.
+     *
+     * @param deploymentId the deployment ID
+     * @return a future completed with the result
+     */
+    public Future<Void> stop(String deploymentId)
+    {
+        LOGGER.info("Stopping Cassandra Sidecar");
+        deployedServerVerticles.clear();
+        Objects.requireNonNull(deploymentId, "deploymentId must not be null");
+        return notifyServerStopping(deploymentId)
+               .compose(v -> vertx.undeploy(deploymentId))
+               .onSuccess(v -> LOGGER.info("Successfully stopped Cassandra Sidecar"));
+    }
+
+    /**
+     * Stops the {@link Vertx} instance and release any resources held by it.
+     *
+     * <p>The instance cannot be used after it has been closed.
+     *
+     * @return a future completed with the result
+     */
+    public Future<Void> close()
+    {
+        LOGGER.info("Stopping Cassandra Sidecar");
+        deployedServerVerticles.clear();
+        List<Future<Void>> closingFutures = new ArrayList<>();
+        closingFutures.add(notifyServerStopping(null));
+
+        Promise<Void> periodicTaskExecutorPromise = Promise.promise();
+        periodicTaskExecutor.close(periodicTaskExecutorPromise);
+        closingFutures.add(periodicTaskExecutorPromise.future());
+
+        instancesConfig.instances()
+                       .forEach(instance ->
+                                closingFutures.add(executorPools.internal()
+                                                                .executeBlocking(promise -> {
+                                                                    instance.delegate().close();
+                                                                    promise.complete(null);
+                                                                })));
+        return Future.all(closingFutures)
+                     .compose(v1 -> executorPools.close())
+                     .onComplete(v -> vertx.close())
+                     .onSuccess(f -> LOGGER.info("Successfully stopped Cassandra Sidecar"));
+    }
+
+    /**
+     * Updates the SSL Options for all servers in all the deployed verticle instances with the {@code timestamp}
+     * of the updated file
+     *
+     * @param timestamp the timestamp of the updated file
+     * @return a future to indicate the update was successfully completed
+     */
+    public Future<CompositeFuture> updateSSLOptions(long timestamp)
+    {
+        SSLOptions options = new SSLOptions();
+        // Sets the updated SSL options
+        optionsProvider.configureSSLOptions(options, sidecarConfiguration.sslConfiguration(), timestamp);
+        // Updates the SSL options of all the deployed verticles
+        List<Future<CompositeFuture>> updateFutures =
+        deployedServerVerticles.stream()
+                               .map(serverVerticle -> serverVerticle.updateSSLOptions(options))
+                               .collect(Collectors.toList());
+        return Future.all(updateFutures);
+    }
+
+    /**
+     * Expose the port of the first deployed verticle for testing purposes
+     *
+     * @return the port where the first verticle is deployed, or -1 if the server has not been deployed
+     */
+    @VisibleForTesting
+    public int actualPort()
+    {
+        if (!deployedServerVerticles.isEmpty())
+            return deployedServerVerticles.get(0).actualPort();
+        return -1;
+    }
+
+    protected Future<String> notifyServerStart(String deploymentId)
+    {
+        LOGGER.info("Successfully started Cassandra Sidecar");
+        vertx.eventBus().publish(SidecarServerEvents.ON_SERVER_START.address(), deploymentId);
+        return Future.succeededFuture(deploymentId);
+    }
+
+    protected Future<Void> notifyServerStopping(String deploymentId)
+    {
+        vertx.eventBus().publish(SidecarServerEvents.ON_SERVER_STOP.address(), deploymentId);
+        return Future.succeededFuture();
+    }
+
+    protected void banner(PrintStream out)
+    {
+        out.println(" _____                               _              _____ _     _                     \n" +
+                    "/  __ \\                             | |            /  ___(_)   | |                    \n" +
+                    "| /  \\/ __ _ ___ ___  __ _ _ __   __| |_ __ __ _   \\ `--. _  __| | ___  ___ __ _ _ __ \n" +
+                    "| |    / _` / __/ __|/ _` | '_ \\ / _` | '__/ _` |   `--. \\ |/ _` |/ _ \\/ __/ _` | '__|\n" +
+                    "| \\__/\\ (_| \\__ \\__ \\ (_| | | | | (_| | | | (_| |  /\\__/ / | (_| |  __/ (_| (_| | |   \n" +
+                    " \\____/\\__,_|___/___/\\__,_|_| |_|\\__,_|_|  \\__,_|  \\____/|_|\\__,_|\\___|\\___\\__,_|_|\n" +
+                    "                                                                                      \n" +
+                    "                                                                                      ");
+    }
+
+    protected void validate()
+    {
+        SslConfiguration ssl = sidecarConfiguration.sslConfiguration();
+        if (ssl == null || !ssl.enabled())
+        {
+            return;
+        }
+
+        try
+        {
+            if (!ssl.isKeystoreConfigured())
+                throw new IllegalArgumentException("keyStorePath and keyStorePassword must be set if ssl enabled");
+
+            SslUtils.validateSslOpts(ssl.keystore());
+
+            if (ssl.isTrustStoreConfigured())
+                SslUtils.validateSslOpts(ssl.truststore());
+        }
+        catch (Exception e)
+        {
+            throw new RuntimeException("Invalid keystore parameters for SSL", e);
+        }
+    }
+
+    /**
+     * Schedules internal {@link org.apache.cassandra.sidecar.tasks.PeriodicTask}s.
+     *
+     * @param deploymentId the deployment ID
+     * @return a succeeded future with the deployment ID of the server
+     */
+    protected Future<String> scheduleInternalPeriodicTasks(String deploymentId)
+    {
+        periodicTaskExecutor.schedule(new HealthCheckPeriodicTask(vertx,
+                                                                  sidecarConfiguration,
+                                                                  instancesConfig,
+                                                                  executorPools));
+        maybeScheduleKeyStoreCheckPeriodicTask();
+
+        MessageConsumer<JsonObject> cqlReadyConsumer = vertx.eventBus().localConsumer(ON_CASSANDRA_CQL_READY.address());
+        cqlReadyConsumer.handler(message -> onCqlReady(cqlReadyConsumer, message));
+
+
+        return Future.succeededFuture(deploymentId);
+    }
+
+    /**
+     * When the SSL configuration is provided and enabled it schedules a periodic task to check for changes
+     * in the keystore file.
+     */
+    protected void maybeScheduleKeyStoreCheckPeriodicTask()
+    {
+        SslConfiguration ssl = sidecarConfiguration.sslConfiguration();
+        if (ssl == null
+            || !ssl.enabled()
+            || !ssl.keystore().isConfigured()
+            || !ssl.keystore().reloadStore())
+        {
+            return;
+        }
+
+        // the checks for the keystore changes are initialized here because we need a reference to the
+        // server to be able to update the SSL options
+        periodicTaskExecutor.schedule(new KeyStoreCheckPeriodicTask(vertx, this, ssl));
+    }
+
+    /**
+     * Handles CQL ready events. When all the expected CQL connections are ready, notifies to the
+     * {@link SidecarServerEvents#ON_ALL_CASSANDRA_CQL_READY} address.
+     *
+     * @param cqlReadyConsumer the consumer
+     * @param message          the received message
+     */
+    protected void onCqlReady(MessageConsumer<JsonObject> cqlReadyConsumer, Message<JsonObject> message)
+    {
+        cqlReadyInstanceIds.add(message.body().getInteger("cassandraInstanceId"));
+
+        boolean isCqlReadyOnAllInstances = instancesConfig.instances().stream()
+                                                          .map(InstanceMetadata::id)
+                                                          .allMatch(cqlReadyInstanceIds::contains);
+        if (isCqlReadyOnAllInstances)
+        {
+            cqlReadyConsumer.unregister(); // stop listening to CQL ready events
+            notifyAllCassandraCqlAreReady();
+            LOGGER.info("CQL is ready for all Cassandra instances. {}", cqlReadyInstanceIds);
+        }
+    }
+
+    /**
+     * Constructs the notification message containing all the Cassandra instance IDs and publishes the message
+     * notifying consumers that all the CQL connections are available.
+     */
+    protected void notifyAllCassandraCqlAreReady()
+    {
+        JsonArray cassandraInstanceIds = new JsonArray();
+        cqlReadyInstanceIds.forEach(cassandraInstanceIds::add);
+        JsonObject allReadyMessage = new JsonObject()
+                                     .put("cassandraInstanceIds", cassandraInstanceIds);
+
+        vertx.eventBus().publish(ON_ALL_CASSANDRA_CQL_READY.address(), allReadyMessage);
+    }
+}
diff --git a/src/main/java/org/apache/cassandra/sidecar/server/ServerVerticle.java b/src/main/java/org/apache/cassandra/sidecar/server/ServerVerticle.java
new file mode 100644
index 0000000..d3a01e9
--- /dev/null
+++ b/src/main/java/org/apache/cassandra/sidecar/server/ServerVerticle.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.server;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.vertx.core.AbstractVerticle;
+import io.vertx.core.CompositeFuture;
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.core.http.HttpServer;
+import io.vertx.core.http.HttpServerOptions;
+import io.vertx.core.net.SSLOptions;
+import io.vertx.core.net.SocketAddress;
+import io.vertx.ext.web.Router;
+import org.apache.cassandra.sidecar.config.ServiceConfiguration;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.jetbrains.annotations.VisibleForTesting;
+
+/**
+ * The verticle implementation for a Sidecar server
+ */
+public class ServerVerticle extends AbstractVerticle
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(ServerVerticle.class);
+
+    protected final SidecarConfiguration sidecarConfiguration;
+    protected final Router router;
+    private final HttpServerOptions options;
+    private List<HttpServer> deployedServers;
+
+    /**
+     * Constructs a new instance of the {@link ServerVerticle} with the provided parameters.
+     *
+     * @param sidecarConfiguration the configuration for running Sidecar
+     * @param router               the configured router for this Service
+     * @param options              the {@link HttpServerOptions} to create the HTTP server
+     */
+    public ServerVerticle(SidecarConfiguration sidecarConfiguration,
+                          Router router,
+                          HttpServerOptions options)
+    {
+        this.sidecarConfiguration = sidecarConfiguration;
+        this.router = router;
+        this.options = options;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void start(Promise<Void> startPromise)
+    {
+        ServiceConfiguration serviceConf = sidecarConfiguration.serviceConfiguration();
+
+        List<SocketAddress> listenSocketAddresses = serviceConf.listenSocketAddresses();
+        LOGGER.info("Deploying Cassandra Sidecar server verticle on socket addresses={}", listenSocketAddresses);
+        List<Future<HttpServer>> futures = listenSocketAddresses.stream()
+                                                                .map(socketAddress -> vertx.createHttpServer(options)
+                                                                                           .requestHandler(router)
+                                                                                           .listen(socketAddress))
+                                                                .collect(Collectors.toList());
+        Future.all(futures)
+              .onSuccess((CompositeFuture startedServerFuture) -> {
+                  deployedServers = new ArrayList<>(startedServerFuture.list());
+                  LOGGER.info("Successfully deployed Cassandra Sidecar server verticle on socket addresses={}",
+                              listenSocketAddresses);
+                  startPromise.complete(); // notify that server started successfully
+              })
+              .onFailure(cause -> {
+                  LOGGER.error("Failed to deploy Cassandra Sidecar verticle failed on socket addresses={}",
+                               listenSocketAddresses, cause);
+                  startPromise.fail(cause); // propagate failure to deploying class
+              });
+    }
+
+    /**
+     * Updates the {@link SSLOptions} internally for the deployed server
+     *
+     * @param options the updated SSL options
+     * @return a future signaling the update success
+     */
+    Future<CompositeFuture> updateSSLOptions(SSLOptions options)
+    {
+        List<HttpServer> deployedServers = this.deployedServers;
+        if (deployedServers == null || deployedServers.isEmpty())
+        {
+            return Future.failedFuture("No servers are running");
+        }
+
+        return Future.all(deployedServers.stream()
+                                         .map(server -> server.updateSSLOptions(options))
+                                         .collect(Collectors.toList()));
+    }
+
+    /**
+     * @return the actual port of the first deployed server, or -1 if no servers are deployed
+     */
+    @VisibleForTesting
+    int actualPort()
+    {
+        if (deployedServers != null && !deployedServers.isEmpty())
+            return deployedServers.get(0).actualPort();
+        return -1;
+    }
+}
diff --git a/src/main/java/org/apache/cassandra/sidecar/server/SidecarServerEvents.java b/src/main/java/org/apache/cassandra/sidecar/server/SidecarServerEvents.java
new file mode 100644
index 0000000..d15423e
--- /dev/null
+++ b/src/main/java/org/apache/cassandra/sidecar/server/SidecarServerEvents.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.server;
+
+/**
+ * Defines the {@link io.vertx.core.eventbus.EventBus} addresses where different notifications will be published
+ * during Sidecar startup/shutdown, as well as CQL connection availability.
+ *
+ * <p>The messages can be published multiple times depending on whether Sidecar is started or stopped
+ * during the lifetime of the application. Implementing consumers will need to deal with this expectation
+ * internally.
+ * <p>
+ * The expectation is that:
+ * <ul>
+ * <li>{@link #ON_SERVER_START} will happen first
+ * <li>{@link #ON_SERVER_STOP} can happen before {@link #ON_ALL_CASSANDRA_CQL_READY}
+ * <li>{@link #ON_SERVER_START} can only happen for any subsequent calls only after a {@link #ON_SERVER_STOP} message
+ * <li>{@link #ON_ALL_CASSANDRA_CQL_READY} might never happen
+ * <li>{@link #ON_CASSANDRA_CQL_READY} can be called multiple times with different cassandraInstanceId values
+ * <li>{@link #ON_CASSANDRA_CQL_DISCONNECTED} can be called multiple times with different cassandraInstanceId values
+ * </ul>
+ * <p>
+ * However, implementers should choose to implement methods assuming no guarantees to the event sequence.
+ */
+public enum SidecarServerEvents
+{
+    /**
+     * The {@link io.vertx.core.eventbus.EventBus} address where server start events will be published. Server start
+     * will be published whenever Sidecar has successfully started and is ready listening for requests.
+     */
+    ON_SERVER_START,
+
+    /**
+     * The {@link io.vertx.core.eventbus.EventBus} address where server stop/shutdown events will be published.
+     * Server stop events will be published whenever Sidecar is stopping or shutting down.
+     */
+    ON_SERVER_STOP,
+
+    /**
+     * The {@link io.vertx.core.eventbus.EventBus} address where events will be published when a CQL connection for
+     * a given instance has been established. The instance identifier will be passed as part of the message.
+     */
+    ON_CASSANDRA_CQL_READY,
+
+    /**
+     * The {@link io.vertx.core.eventbus.EventBus} address where events will be published when a CQL connection for
+     * a given instance has been disconnected. The instance identifier will be passed as part of the message.
+     */
+    ON_CASSANDRA_CQL_DISCONNECTED,
+
+    /**
+     * The {@link io.vertx.core.eventbus.EventBus} address where events will be published when all CQL connections
+     * for the Sidecar-managed Cassandra instances are available.
+     */
+    ON_ALL_CASSANDRA_CQL_READY,
+    ;
+
+    public String address()
+    {
+        return SidecarServerEvents.class.getName() + "." + name();
+    }
+}
diff --git a/src/main/java/org/apache/cassandra/sidecar/snapshots/SnapshotPathBuilder.java b/src/main/java/org/apache/cassandra/sidecar/snapshots/SnapshotPathBuilder.java
index 2077f0f..655b840 100644
--- a/src/main/java/org/apache/cassandra/sidecar/snapshots/SnapshotPathBuilder.java
+++ b/src/main/java/org/apache/cassandra/sidecar/snapshots/SnapshotPathBuilder.java
@@ -38,7 +38,6 @@
 
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import io.vertx.core.CompositeFuture;
 import io.vertx.core.Future;
 import io.vertx.core.Promise;
 import io.vertx.core.Vertx;
@@ -140,79 +139,77 @@
               logger.debug("Found {} files in snapshot directory '{}'", list.size(), snapshotDirectory);
 
               // Prepare futures to get properties for all the files from listing the snapshot directory
-              //noinspection rawtypes
-              List<Future> futures = list.stream()
-                                         .map(fs::props)
-                                         .collect(Collectors.toList());
+              List<Future<FileProps>> futures = list.stream()
+                                                    .map(fs::props)
+                                                    .collect(Collectors.toList());
 
-              CompositeFuture.all(futures)
-                             .onFailure(cause -> {
-                                 logger.debug("Failed to get FileProps", cause);
-                                 promise.fail(cause);
-                             })
-                             .onSuccess(ar -> {
+              Future.all(futures)
+                    .onFailure(cause -> {
+                        logger.debug("Failed to get FileProps", cause);
+                        promise.fail(cause);
+                    })
+                    .onSuccess(ar -> {
 
-                                 // Create a pair of path/fileProps for every regular file
-                                 List<SnapshotFile> snapshotList =
-                                 IntStream.range(0, list.size())
-                                          .filter(i -> ar.<FileProps>resultAt(i).isRegularFile())
-                                          .mapToObj(i -> {
-                                              long size = ar.<FileProps>resultAt(i).size();
-                                              return new SnapshotFile(list.get(i),
-                                                                      size);
-                                          })
-                                          .collect(Collectors.toList());
+                        // Create a pair of path/fileProps for every regular file
+                        List<SnapshotFile> snapshotList =
+                        IntStream.range(0, list.size())
+                                 .filter(i -> ar.<FileProps>resultAt(i).isRegularFile())
+                                 .mapToObj(i -> {
+                                     long size = ar.<FileProps>resultAt(i).size();
+                                     return new SnapshotFile(list.get(i),
+                                                             size);
+                                 })
+                                 .collect(Collectors.toList());
 
 
-                                 if (!includeSecondaryIndexFiles)
-                                 {
-                                     // We are done if we don't include secondary index files
-                                     promise.complete(snapshotList);
-                                     return;
-                                 }
+                        if (!includeSecondaryIndexFiles)
+                        {
+                            // We are done if we don't include secondary index files
+                            promise.complete(snapshotList);
+                            return;
+                        }
 
-                                 // Find index directories and prepare futures listing the snapshot directory
-                                 //noinspection rawtypes
-                                 List<Future> idxListFutures =
-                                 IntStream.range(0, list.size())
-                                          .filter(i -> {
-                                              if (ar.<FileProps>resultAt(i).isDirectory())
-                                              {
-                                                  Path path = Paths.get(list.get(i));
-                                                  int count = path.getNameCount();
-                                                  return count > 0
-                                                         && path.getName(count - 1)
-                                                                .toString()
-                                                                .startsWith(".");
-                                              }
-                                              return false;
-                                          })
-                                          .mapToObj(i -> listSnapshotDirectory(list.get(i), false))
-                                          .collect(Collectors.toList());
-                                 if (idxListFutures.isEmpty())
-                                 {
-                                     // If there are no secondary index directories we are done
-                                     promise.complete(snapshotList);
-                                     return;
-                                 }
-                                 logger.debug("Found {} index directories in the '{}' snapshot",
-                                              idxListFutures.size(), snapshotDirectory);
-                                 // if we have index directories, list them all
-                                 CompositeFuture.all(idxListFutures)
-                                                .onFailure(promise::fail)
-                                                .onSuccess(idx -> {
-                                                    //noinspection unchecked
-                                                    List<SnapshotFile> idxPropList =
-                                                    idx.list()
-                                                       .stream()
-                                                       .flatMap(l -> ((List<SnapshotFile>) l).stream())
-                                                       .collect(Collectors.toList());
+                        // Find index directories and prepare futures listing the snapshot directory
+                        List<Future<List<SnapshotFile>>> idxListFutures =
+                        IntStream.range(0, list.size())
+                                 .filter(i -> {
+                                     if (ar.<FileProps>resultAt(i).isDirectory())
+                                     {
+                                         Path path = Paths.get(list.get(i));
+                                         int count = path.getNameCount();
+                                         return count > 0
+                                                && path.getName(count - 1)
+                                                       .toString()
+                                                       .startsWith(".");
+                                     }
+                                     return false;
+                                 })
+                                 .mapToObj(i -> listSnapshotDirectory(list.get(i), false))
+                                 .collect(Collectors.toList());
+                        if (idxListFutures.isEmpty())
+                        {
+                            // If there are no secondary index directories we are done
+                            promise.complete(snapshotList);
+                            return;
+                        }
+                        logger.debug("Found {} index directories in the '{}' snapshot",
+                                     idxListFutures.size(), snapshotDirectory);
+                        // if we have index directories, list them all
+                        Future.all(idxListFutures)
+                              .onFailure(promise::fail)
+                              .onSuccess(idx -> {
+                                  //noinspection unchecked
+                                  List<SnapshotFile> idxPropList =
+                                  idx.list()
+                                     .stream()
+                                     .flatMap(l -> ((List<SnapshotFile>) l).stream())
+                                     .collect(Collectors.toList());
 
-                                                    // aggregate the results and return the full list
-                                                    snapshotList.addAll(idxPropList);
-                                                    promise.complete(snapshotList);
-                                                });
-                             });
+                                  // aggregate the results and return the full list
+                                  snapshotList.addAll(idxPropList);
+                                  promise.complete(snapshotList);
+                              });
+                    });
           });
         return promise.future();
     }
@@ -421,39 +418,38 @@
      */
     protected Future<String> lastModifiedTableDirectory(List<String> fileList, String tableName)
     {
-        if (fileList.size() == 0)
+        if (fileList.isEmpty())
         {
             String errMsg = String.format("Table '%s' does not exist", tableName);
             return Future.failedFuture(new NoSuchFileException(errMsg));
         }
 
-        //noinspection rawtypes
-        List<Future> futures = fileList.stream()
-                                       .map(fs::props)
-                                       .collect(Collectors.toList());
+        List<Future<FileProps>> futures = fileList.stream()
+                                                  .map(fs::props)
+                                                  .collect(Collectors.toList());
 
         Promise<String> promise = Promise.promise();
-        CompositeFuture.all(futures)
-                       .onFailure(promise::fail)
-                       .onSuccess(ar -> {
-                           String directory = IntStream.range(0, fileList.size())
-                                                       .mapToObj(i -> pair(fileList.get(i), ar.<FileProps>resultAt(i)))
-                                                       .filter(pair -> pair.getValue().isDirectory())
-                                                       .max(Comparator.comparingLong(pair -> pair.getValue()
-                                                                                                 .lastModifiedTime()))
-                                                       .map(AbstractMap.SimpleEntry::getKey)
-                                                       .orElse(null);
+        Future.all(futures)
+              .onFailure(promise::fail)
+              .onSuccess(ar -> {
+                  String directory = IntStream.range(0, fileList.size())
+                                              .mapToObj(i -> pair(fileList.get(i), ar.<FileProps>resultAt(i)))
+                                              .filter(pair -> pair.getValue().isDirectory())
+                                              .max(Comparator.comparingLong(pair -> pair.getValue()
+                                                                                        .lastModifiedTime()))
+                                              .map(AbstractMap.SimpleEntry::getKey)
+                                              .orElse(null);
 
-                           if (directory == null)
-                           {
-                               String errMsg = String.format("Table '%s' does not exist", tableName);
-                               promise.fail(new NoSuchFileException(errMsg));
-                           }
-                           else
-                           {
-                               promise.complete(directory);
-                           }
-                       });
+                  if (directory == null)
+                  {
+                      String errMsg = String.format("Table '%s' does not exist", tableName);
+                      promise.fail(new NoSuchFileException(errMsg));
+                  }
+                  else
+                  {
+                      promise.complete(directory);
+                  }
+              });
         return promise.future();
     }
 
diff --git a/src/main/java/org/apache/cassandra/sidecar/tasks/HealthCheckPeriodicTask.java b/src/main/java/org/apache/cassandra/sidecar/tasks/HealthCheckPeriodicTask.java
new file mode 100644
index 0000000..84cf07a
--- /dev/null
+++ b/src/main/java/org/apache/cassandra/sidecar/tasks/HealthCheckPeriodicTask.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.tasks;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.vertx.core.Promise;
+import io.vertx.core.Vertx;
+import io.vertx.core.eventbus.EventBus;
+import org.apache.cassandra.sidecar.cluster.InstancesConfig;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+
+import static org.apache.cassandra.sidecar.server.SidecarServerEvents.ON_SERVER_START;
+import static org.apache.cassandra.sidecar.server.SidecarServerEvents.ON_SERVER_STOP;
+
+/**
+ * Periodically checks the health of every instance configured in the {@link InstancesConfig}.
+ */
+public class HealthCheckPeriodicTask implements PeriodicTask
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(HealthCheckPeriodicTask.class);
+    private final EventBus eventBus;
+    private final SidecarConfiguration configuration;
+    private final InstancesConfig instancesConfig;
+    private final ExecutorPools.TaskExecutorPool internalPool;
+
+    public HealthCheckPeriodicTask(Vertx vertx,
+                                   SidecarConfiguration configuration,
+                                   InstancesConfig instancesConfig,
+                                   ExecutorPools executorPools)
+    {
+        eventBus = vertx.eventBus();
+        this.configuration = configuration;
+        this.instancesConfig = instancesConfig;
+        internalPool = executorPools.internal();
+    }
+
+    @Override
+    public void registerPeriodicTaskExecutor(PeriodicTaskExecutor executor)
+    {
+        eventBus.localConsumer(ON_SERVER_START.address(), message -> executor.schedule(this));
+        eventBus.localConsumer(ON_SERVER_STOP.address(), message -> executor.unschedule(this));
+    }
+
+    @Override
+    public long initialDelay()
+    {
+        return configuration.healthCheckConfiguration().initialDelayMillis();
+    }
+
+    @Override
+    public long delay()
+    {
+        return configuration.healthCheckConfiguration().checkIntervalMillis();
+    }
+
+    /**
+     * Run health checks on all the configured instances
+     */
+    @Override
+    public void execute(Promise<Void> promise)
+    {
+        List<InstanceMetadata> instances = instancesConfig.instances();
+        AtomicInteger counter = new AtomicInteger(instances.size());
+        instances.forEach(instanceMetadata -> internalPool.executeBlocking(p -> {
+            try
+            {
+                instanceMetadata.delegate().healthCheck();
+                p.complete();
+            }
+            catch (Throwable cause)
+            {
+                p.fail(cause);
+                LOGGER.error("Unable to complete health check on instance={}",
+                             instanceMetadata.id(), cause);
+            }
+            finally
+            {
+                if (counter.decrementAndGet() == 0)
+                {
+                    promise.tryComplete();
+                }
+            }
+        }, false));
+    }
+
+    @Override
+    public String name()
+    {
+        return "Health Check";
+    }
+}
diff --git a/src/main/java/org/apache/cassandra/sidecar/tasks/KeyStoreCheckPeriodicTask.java b/src/main/java/org/apache/cassandra/sidecar/tasks/KeyStoreCheckPeriodicTask.java
new file mode 100644
index 0000000..d9744f8
--- /dev/null
+++ b/src/main/java/org/apache/cassandra/sidecar/tasks/KeyStoreCheckPeriodicTask.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.tasks;
+
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.vertx.core.Promise;
+import io.vertx.core.Vertx;
+import org.apache.cassandra.sidecar.config.SslConfiguration;
+import org.apache.cassandra.sidecar.server.Server;
+
+/**
+ * Periodically checks whether the key store file has changed. Triggers an update to the server's SSLOptions
+ * whenever a file change has detected.
+ */
+public class KeyStoreCheckPeriodicTask implements PeriodicTask
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(KeyStoreCheckPeriodicTask.class);
+
+    private final Vertx vertx;
+    private final Server server;
+    private final SslConfiguration configuration;
+    private long lastModifiedTime = 0; // records the last modified timestamp
+
+    public KeyStoreCheckPeriodicTask(Vertx vertx, Server server, SslConfiguration configuration)
+    {
+        this.vertx = vertx;
+        this.server = server;
+        this.configuration = configuration;
+        maybeRecordLastModifiedTime();
+    }
+
+    /**
+     * Skip check if the key store is not configured or if the key store should not be reloaded
+     *
+     * @return {@code true} if the key store is not configured or if the keystore should not be reloaded,
+     * {@code false} otherwise
+     */
+    @Override
+    public boolean shouldSkip()
+    {
+        return !configuration.isKeystoreConfigured()
+               || !configuration.keystore().reloadStore();
+    }
+
+    @Override
+    public long delay()
+    {
+        return configuration.keystore().checkIntervalInSeconds();
+    }
+
+    @Override
+    public TimeUnit delayUnit()
+    {
+        return TimeUnit.SECONDS;
+    }
+
+    @Override
+    public void execute(Promise<Void> promise)
+    {
+        LOGGER.info("Running periodic key store checker");
+        String keyStorePath = configuration.keystore().path();
+        vertx.fileSystem().props(keyStorePath)
+             .onSuccess(props -> {
+                 long previousLastModifiedTime = lastModifiedTime;
+                 if (props.lastModifiedTime() != previousLastModifiedTime)
+                 {
+                     LOGGER.info("Certificate file change detected for path={}, previousLastModifiedTime={}, " +
+                                 "lastModifiedTime={}", keyStorePath, previousLastModifiedTime,
+                                 props.lastModifiedTime());
+
+                     server.updateSSLOptions(props.lastModifiedTime())
+                           .onSuccess(v -> {
+                               lastModifiedTime = props.lastModifiedTime();
+                               LOGGER.info("Completed reloading certificates from path={}", keyStorePath);
+                               promise.complete(); // propagate successful completion
+                           })
+                           .onFailure(cause -> {
+                               LOGGER.error("Failed to reload certificate from path={}", keyStorePath, cause);
+                               promise.fail(cause);
+                           });
+                 }
+                 else
+                 {
+                     promise.complete(); // make sure to fulfill the promise
+                 }
+             })
+             .onFailure(error -> {
+                 LOGGER.warn("Unable to retrieve props for path={}", keyStorePath, error);
+                 promise.fail(error);
+             });
+    }
+
+    protected void maybeRecordLastModifiedTime()
+    {
+        if (shouldSkip())
+        {
+            return;
+        }
+        String keyStorePath = configuration.keystore().path();
+        vertx.fileSystem().props(keyStorePath)
+             .onSuccess(props -> lastModifiedTime = props.lastModifiedTime())
+             .onFailure(err -> {
+                 LOGGER.error("Unable to get lastModifiedTime for path={}", keyStorePath);
+                 lastModifiedTime = -1;
+             });
+    }
+}
diff --git a/src/main/java/org/apache/cassandra/sidecar/tasks/PeriodicTask.java b/src/main/java/org/apache/cassandra/sidecar/tasks/PeriodicTask.java
new file mode 100644
index 0000000..7021e53
--- /dev/null
+++ b/src/main/java/org/apache/cassandra/sidecar/tasks/PeriodicTask.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.tasks;
+
+import java.util.concurrent.TimeUnit;
+
+import io.vertx.core.Promise;
+
+/**
+ * An interface that defines a periodic task that will be executed during the lifecycle of Cassandra Sidecar
+ */
+public interface PeriodicTask
+{
+    /**
+     * @return delay in the specified {@link #delayUnit()} for periodic task
+     */
+    long delay();
+
+    /**
+     * @return the unit for the {@link #delay()}, if not specified defaults to milliseconds
+     */
+    default TimeUnit delayUnit()
+    {
+        return TimeUnit.MILLISECONDS;
+    }
+
+    /**
+     * @return the initial delay for the task, defaults to the {@link #delay()}
+     */
+    default long initialDelay()
+    {
+        return delay();
+    }
+
+    /**
+     * @return the units for the {@link #initialDelay()}, if not specified defaults to {@link #delayUnit()}
+     */
+    default TimeUnit initialDelayUnit()
+    {
+        return delayUnit();
+    }
+
+    /**
+     * Defines the task body.
+     * The method can be considered as executing in a single thread.
+     *
+     * @param promise a promise when the execution completes
+     */
+    void execute(Promise<Void> promise);
+
+    /**
+     * Register the periodic task executor at the task. By default, it is no-op.
+     * If the reference to the executor is needed, the concrete {@link PeriodicTask} can implement this method
+     *
+     * @param executor the executor that manages the task
+     */
+    default void registerPeriodicTaskExecutor(PeriodicTaskExecutor executor)
+    {
+    }
+
+    /**
+     * Specify whether the task should be skipped.
+     *
+     * @return {@code true} to skip; otherwise, return {@code false}
+     */
+    default boolean shouldSkip()
+    {
+        return false;
+    }
+
+    /**
+     * Close any resources it opened.
+     * Implementation note: it is encouraged to handle the exceptions during close()
+     */
+    default void close()
+    {
+    }
+
+    /**
+     * @return descriptive name of the task. It prefers simple class name, if it is non-empty;
+     * otherwise, it returns the full class name
+     */
+    default String name()
+    {
+        String simpleName = this.getClass().getSimpleName();
+        return simpleName.isEmpty() ? this.getClass().getName() : simpleName;
+    }
+}
diff --git a/src/main/java/org/apache/cassandra/sidecar/tasks/PeriodicTaskExecutor.java b/src/main/java/org/apache/cassandra/sidecar/tasks/PeriodicTaskExecutor.java
new file mode 100644
index 0000000..898e037
--- /dev/null
+++ b/src/main/java/org/apache/cassandra/sidecar/tasks/PeriodicTaskExecutor.java
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.tasks;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.vertx.core.Closeable;
+import io.vertx.core.Promise;
+import io.vertx.core.impl.ConcurrentHashSet;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+
+/**
+ * This class manages the scheduling and execution of {@link PeriodicTask}s.
+ */
+@Singleton
+public class PeriodicTaskExecutor implements Closeable
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(PeriodicTaskExecutor.class);
+
+    private final Map<PeriodicTaskKey, Long> timerIds = new ConcurrentHashMap<>();
+    private final Set<PeriodicTaskKey> activeTasks = new ConcurrentHashSet<>();
+    private final ExecutorPools.TaskExecutorPool internalPool;
+
+    @Inject
+    public PeriodicTaskExecutor(ExecutorPools executorPools)
+    {
+        this.internalPool = executorPools.internal();
+    }
+
+    /**
+     * Schedules the {@code task} iff it has not been scheduled yet.
+     *
+     * @param task the task to execute
+     * @return the identifier of the timer associated with this task
+     */
+    public long schedule(PeriodicTask task)
+    {
+        PeriodicTaskKey key = new PeriodicTaskKey(task);
+        return timerIds.computeIfAbsent(key, k -> {
+            task.registerPeriodicTaskExecutor(this);
+            long initialDelayMillis = task.initialDelayUnit().toMillis(task.initialDelay());
+            long delayMillis = task.delayUnit().toMillis(task.delay());
+            return internalPool.setPeriodic(initialDelayMillis, delayMillis, id -> executeInternal(k));
+        });
+    }
+
+    /**
+     * Unschedules the {@code task} iff the task has been registered previously and returns the identifier
+     * of the timer associated with this task, or {@code -1} if the task is not registered.
+     *
+     * @param task the task to unschedule
+     * @return the identifier of the timer associated with this task, or {@code -1} if the task is not registered
+     */
+    public long unschedule(PeriodicTask task)
+    {
+        Long timerId = timerIds.remove(new PeriodicTaskKey(task));
+        if (timerId != null)
+        {
+            internalPool.cancelTimer(timerId);
+            task.close();
+            return timerId;
+        }
+        return -1L; // valid timer ids are non-negative
+    }
+
+    /**
+     * Reschedules the provided {@code task} and returns the identifier of the timer associated with the rescheduled
+     * task.
+     *
+     * @param task the task to reschedule
+     * @return the identifier of the timer associated with the rescheduled task
+     */
+    public long reschedule(PeriodicTask task)
+    {
+        Long timerId = timerIds.remove(new PeriodicTaskKey(task));
+        if (timerId != null)
+        {
+            internalPool.cancelTimer(timerId);
+        }
+        return schedule(task);
+    }
+
+    @Override
+    public void close(Promise<Void> completion)
+    {
+        try
+        {
+            timerIds.values().forEach(internalPool::cancelTimer);
+            timerIds.keySet().forEach(key -> key.task.close());
+            timerIds.clear();
+            activeTasks.clear();
+            completion.complete();
+        }
+        catch (Throwable throwable)
+        {
+            completion.fail(throwable);
+        }
+    }
+
+    private void executeInternal(PeriodicTaskKey key)
+    {
+        PeriodicTask periodicTask = key.task;
+        if (periodicTask.shouldSkip())
+        {
+            LOGGER.trace("Skip executing task. task={}", periodicTask.name());
+            return;
+        }
+
+        if (!activeTasks.add(key))
+        {
+            LOGGER.debug("Task is already running. task={}", periodicTask.name());
+            return;
+        }
+
+        Promise<Void> promise = Promise.promise();
+
+        try
+        {
+            periodicTask.execute(promise);
+        }
+        catch (Throwable throwable)
+        {
+            LOGGER.warn("Periodic task failed to execute. task={}", periodicTask.name(), throwable);
+            promise.tryFail(throwable);
+        }
+
+        promise.future().onComplete(res -> activeTasks.remove(key));
+    }
+
+    // A simple wrapper that implements equals and hashcode,
+    // which is not necessary for the actual ExecutionLoops to implement
+    private static class PeriodicTaskKey
+    {
+        private final String fqcnAndName;
+        private final PeriodicTask task;
+
+        PeriodicTaskKey(PeriodicTask task)
+        {
+            this.fqcnAndName = task.getClass().getCanonicalName() + task.name();
+            this.task = task;
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return fqcnAndName.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object obj)
+        {
+            if (this == obj)
+                return true;
+
+            if (obj instanceof PeriodicTaskKey)
+            {
+                return ((PeriodicTaskKey) obj).fqcnAndName.equals(this.fqcnAndName);
+            }
+
+            return false;
+        }
+    }
+}
diff --git a/common/src/main/java/org/apache/cassandra/sidecar/common/CassandraVersionProvider.java b/src/main/java/org/apache/cassandra/sidecar/utils/CassandraVersionProvider.java
similarity index 94%
rename from common/src/main/java/org/apache/cassandra/sidecar/common/CassandraVersionProvider.java
rename to src/main/java/org/apache/cassandra/sidecar/utils/CassandraVersionProvider.java
index 518bb0e..eb86040 100644
--- a/common/src/main/java/org/apache/cassandra/sidecar/common/CassandraVersionProvider.java
+++ b/src/main/java/org/apache/cassandra/sidecar/utils/CassandraVersionProvider.java
@@ -16,12 +16,13 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.sidecar.common;
+package org.apache.cassandra.sidecar.utils;
 
 import java.util.ArrayList;
 import java.util.List;
 import javax.annotation.concurrent.NotThreadSafe;
 
+import org.apache.cassandra.sidecar.common.ICassandraFactory;
 import org.jetbrains.annotations.VisibleForTesting;
 
 
@@ -79,7 +80,7 @@
      * @throws IllegalArgumentException if the provided string does not
      *                                  represent a version
      */
-    ICassandraFactory cassandra(String requestedVersion)
+    public ICassandraFactory cassandra(String requestedVersion)
     {
         SimpleCassandraVersion version = SimpleCassandraVersion.create(requestedVersion);
         return cassandra(version);
diff --git a/src/main/java/org/apache/cassandra/sidecar/utils/FileStreamer.java b/src/main/java/org/apache/cassandra/sidecar/utils/FileStreamer.java
index 358767b..77cd16f 100644
--- a/src/main/java/org/apache/cassandra/sidecar/utils/FileStreamer.java
+++ b/src/main/java/org/apache/cassandra/sidecar/utils/FileStreamer.java
@@ -51,7 +51,6 @@
 public class FileStreamer
 {
     private static final Logger LOGGER = LoggerFactory.getLogger(FileStreamer.class);
-    private static final long DEFAULT_RATE_LIMIT_STREAM_REQUESTS_PER_SECOND = Long.MAX_VALUE;
 
     private final ExecutorPools executorPools;
     private final ThrottleConfiguration config;
@@ -121,7 +120,7 @@
                                 Instant startTime,
                                 Promise<Void> promise)
     {
-        if (!isRateLimited() || acquire(response, filename, fileLength, range, startTime, promise))
+        if (acquire(response, filename, fileLength, range, startTime, promise))
         {
             // Stream data if rate limiting is disabled or if we acquire
             LOGGER.debug("Streaming range {} for file {} to client {}. Instance: {}", range, filename,
@@ -186,14 +185,6 @@
     }
 
     /**
-     * @return true if this request is rate-limited, false otherwise
-     */
-    private boolean isRateLimited()
-    {
-        return config.rateLimitStreamRequestsPerSecond() != DEFAULT_RATE_LIMIT_STREAM_REQUESTS_PER_SECOND;
-    }
-
-    /**
      * @param startTime the request start time
      * @return true if we exhausted the retries, false otherwise
      */
diff --git a/src/main/java/org/apache/cassandra/sidecar/utils/InstanceMetadataFetcher.java b/src/main/java/org/apache/cassandra/sidecar/utils/InstanceMetadataFetcher.java
index f8661e5..0c51bf0 100644
--- a/src/main/java/org/apache/cassandra/sidecar/utils/InstanceMetadataFetcher.java
+++ b/src/main/java/org/apache/cassandra/sidecar/utils/InstanceMetadataFetcher.java
@@ -20,9 +20,9 @@
 
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.cluster.InstancesConfig;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
 import org.jetbrains.annotations.Nullable;
 
 /**
diff --git a/src/main/java/org/apache/cassandra/sidecar/utils/SSTableImporter.java b/src/main/java/org/apache/cassandra/sidecar/utils/SSTableImporter.java
index a950acc..eb3b29a 100644
--- a/src/main/java/org/apache/cassandra/sidecar/utils/SSTableImporter.java
+++ b/src/main/java/org/apache/cassandra/sidecar/utils/SSTableImporter.java
@@ -37,7 +37,7 @@
 import io.vertx.core.Promise;
 import io.vertx.core.Vertx;
 import io.vertx.ext.web.handler.HttpException;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.common.TableOperations;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
 import org.apache.cassandra.sidecar.config.ServiceConfiguration;
diff --git a/src/main/java/org/apache/cassandra/sidecar/utils/SSTableUploader.java b/src/main/java/org/apache/cassandra/sidecar/utils/SSTableUploader.java
index 0f21a47..465f0a4 100644
--- a/src/main/java/org/apache/cassandra/sidecar/utils/SSTableUploader.java
+++ b/src/main/java/org/apache/cassandra/sidecar/utils/SSTableUploader.java
@@ -21,19 +21,24 @@
 import java.io.File;
 import java.nio.file.AtomicMoveNotSupportedException;
 
+import com.google.common.util.concurrent.SidecarRateLimiter;
 import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import io.vertx.core.AsyncResult;
 import io.vertx.core.Future;
+import io.vertx.core.Handler;
 import io.vertx.core.Vertx;
 import io.vertx.core.buffer.Buffer;
 import io.vertx.core.file.CopyOptions;
 import io.vertx.core.file.FileSystem;
 import io.vertx.core.file.OpenOptions;
 import io.vertx.core.streams.ReadStream;
+import io.vertx.core.streams.WriteStream;
 
 /**
  * A class that handles SSTable Uploads
@@ -46,18 +51,23 @@
 
     private final FileSystem fs;
     private final ChecksumVerifier checksumVerifier;
+    private final SidecarRateLimiter rateLimiter;
 
     /**
      * Constructs an instance of {@link SSTableUploader} with provided params for uploading an SSTable component.
      *
      * @param vertx            Vertx reference
      * @param checksumVerifier verifier for checking integrity of upload
+     * @param rateLimiter      rate limiter for uploading SSTable components
      */
     @Inject
-    public SSTableUploader(Vertx vertx, ChecksumVerifier checksumVerifier)
+    public SSTableUploader(Vertx vertx,
+                           ChecksumVerifier checksumVerifier,
+                           @Named("IngressFileRateLimiter") SidecarRateLimiter rateLimiter)
     {
         this.fs = vertx.fileSystem();
         this.checksumVerifier = checksumVerifier;
+        this.rateLimiter = rateLimiter;
     }
 
     /**
@@ -98,6 +108,7 @@
     {
         LOGGER.debug("Uploading data to={}", tempFilename);
         return fs.open(tempFilename, new OpenOptions()) // open the temp file
+                 .map(file -> new RateLimitedWriteStream(rateLimiter, file))
                  .compose(file -> {
                      readStream.resume();
                      return readStream.pipeTo(file);
@@ -152,4 +163,82 @@
         }
         return false;
     }
+
+
+    /**
+     * A {@link WriteStream} implementation that supports rate limiting.
+     */
+    public static class RateLimitedWriteStream implements WriteStream<Buffer>
+    {
+        private final SidecarRateLimiter limiter;
+        private final WriteStream<Buffer> delegate;
+
+        public RateLimitedWriteStream(SidecarRateLimiter limiter, WriteStream<Buffer> delegate)
+        {
+            this.limiter = limiter;
+            this.delegate = delegate;
+        }
+
+        @Override
+        public WriteStream<Buffer> exceptionHandler(Handler<Throwable> handler)
+        {
+            return delegate.exceptionHandler(handler);
+        }
+
+        @Override
+        public Future<Void> write(Buffer data)
+        {
+            limiter.acquire(data.length()); // apply backpressure on the received bytes
+            return delegate.write(data);
+        }
+
+        @Override
+        public void write(Buffer data, Handler<AsyncResult<Void>> handler)
+        {
+            limiter.acquire(data.length()); // apply backpressure on the received bytes
+            delegate.write(data, handler);
+        }
+
+        @Override
+        public Future<Void> end()
+        {
+            return delegate.end();
+        }
+
+        @Override
+        public void end(Handler<AsyncResult<Void>> handler)
+        {
+            delegate.end(handler);
+        }
+
+        @Override
+        public Future<Void> end(Buffer data)
+        {
+            return delegate.end(data);
+        }
+
+        @Override
+        public void end(Buffer data, Handler<AsyncResult<Void>> handler)
+        {
+            delegate.end(data, handler);
+        }
+
+        @Override
+        public WriteStream<Buffer> setWriteQueueMaxSize(int maxSize)
+        {
+            return delegate.setWriteQueueMaxSize(maxSize);
+        }
+
+        @Override
+        public boolean writeQueueFull()
+        {
+            return delegate.writeQueueFull();
+        }
+
+        @Override
+        public WriteStream<Buffer> drainHandler(Handler<Void> handler)
+        {
+            return delegate.drainHandler(handler);
+        }
+    }
 }
diff --git a/common/src/main/java/org/apache/cassandra/sidecar/common/SimpleCassandraVersion.java b/src/main/java/org/apache/cassandra/sidecar/utils/SimpleCassandraVersion.java
similarity index 96%
rename from common/src/main/java/org/apache/cassandra/sidecar/common/SimpleCassandraVersion.java
rename to src/main/java/org/apache/cassandra/sidecar/utils/SimpleCassandraVersion.java
index 830dddc..2a3d1b4 100644
--- a/common/src/main/java/org/apache/cassandra/sidecar/common/SimpleCassandraVersion.java
+++ b/src/main/java/org/apache/cassandra/sidecar/utils/SimpleCassandraVersion.java
@@ -16,12 +16,15 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.sidecar.common;
+package org.apache.cassandra.sidecar.utils;
 
 import java.util.Objects;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import org.apache.cassandra.sidecar.common.ICassandraFactory;
+import org.apache.cassandra.sidecar.common.MinimumVersion;
+
 /**
  * Implements versioning used in Cassandra and CQL.
  * <p>
diff --git a/src/main/java/org/apache/cassandra/sidecar/utils/SslUtils.java b/src/main/java/org/apache/cassandra/sidecar/utils/SslUtils.java
index 70e9cbf..56abe27 100644
--- a/src/main/java/org/apache/cassandra/sidecar/utils/SslUtils.java
+++ b/src/main/java/org/apache/cassandra/sidecar/utils/SslUtils.java
@@ -24,6 +24,18 @@
 import java.security.KeyStoreException;
 import java.security.NoSuchAlgorithmException;
 import java.security.cert.CertificateException;
+import java.util.Objects;
+import java.util.function.Function;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.X509KeyManager;
+
+import io.vertx.core.Vertx;
+import io.vertx.core.net.JksOptions;
+import io.vertx.core.net.KeyCertOptions;
+import io.vertx.core.net.PfxOptions;
+import io.vertx.core.net.SSLOptions;
+import io.vertx.core.net.TrustOptions;
+import org.apache.cassandra.sidecar.config.KeyStoreConfiguration;
 
 /**
  * Utility class for SSL related operations
@@ -33,30 +45,133 @@
     /**
      * Given the parameters, validate the keystore can be loaded and is usable
      *
-     * @param keyStorePath     the path to the keystore
-     * @param keystorePassword the password for the keystore
+     * @param config the keystore configuration
      * @throws KeyStoreException        when there is an error accessing the keystore
      * @throws NoSuchAlgorithmException when the keystore type algorithm is not available
      * @throws IOException              when an IO exception occurs
      * @throws CertificateException     when a problem was encountered with the certificate
      */
-    public static void validateSslOpts(String keyStorePath, String keystorePassword) throws KeyStoreException,
-                                                                                            NoSuchAlgorithmException,
-                                                                                            IOException,
-                                                                                            CertificateException
+    public static void validateSslOpts(KeyStoreConfiguration config) throws KeyStoreException,
+                                                                            NoSuchAlgorithmException,
+                                                                            IOException,
+                                                                            CertificateException
     {
-        final KeyStore ks;
+        Objects.requireNonNull(config, "config must be provided");
 
-        if (keyStorePath.endsWith("p12"))
+        KeyStore ks;
+
+        if (config.type() != null)
+            ks = KeyStore.getInstance(config.type());
+        else if (config.path().endsWith("p12"))
             ks = KeyStore.getInstance("PKCS12");
-        else if (keyStorePath.endsWith("jks"))
+        else if (config.path().endsWith("jks"))
             ks = KeyStore.getInstance("JKS");
         else
             throw new IllegalArgumentException("Unrecognized keystore format extension: "
-                                               + keyStorePath.substring(keyStorePath.length() - 3));
-        try (FileInputStream keystore = new FileInputStream(keyStorePath))
+                                               + config.path().substring(config.path().length() - 3));
+        try (FileInputStream keystore = new FileInputStream(config.path()))
         {
-            ks.load(keystore, keystorePassword.toCharArray());
+            ks.load(keystore, config.password().toCharArray());
+        }
+    }
+
+    public static void setKeyStoreConfiguration(SSLOptions options, KeyStoreConfiguration keystore, long timestamp)
+    {
+        KeyCertOptions keyCertOptions;
+        switch (keystore.type())
+        {
+            case "JKS":
+                keyCertOptions = new JksOptions().setPath(keystore.path()).setPassword(keystore.password());
+                break;
+
+            case "PKCS12":
+                keyCertOptions = new PfxOptions().setPath(keystore.path()).setPassword(keystore.password());
+                break;
+
+            default:
+                throw new UnsupportedOperationException("KeyStore with type " + keystore.type() + " is not supported");
+        }
+        options.setKeyCertOptions(new WrappedKeyCertOptions(timestamp, keyCertOptions));
+    }
+
+    public static void setTrustStoreConfiguration(SSLOptions options, KeyStoreConfiguration truststore)
+    {
+        TrustOptions keyCertOptions;
+        switch (truststore.type())
+        {
+            case "JKS":
+                keyCertOptions = new JksOptions().setPath(truststore.path()).setPassword(truststore.password());
+                break;
+
+            case "PKCS12":
+                keyCertOptions = new PfxOptions().setPath(truststore.path()).setPassword(truststore.password());
+                break;
+
+            default:
+                throw new UnsupportedOperationException("TrustStore with type " + truststore.type()
+                                                        + " is not supported");
+        }
+        options.setTrustOptions(keyCertOptions);
+    }
+
+    /**
+     * Vertx makes a determination on whether the SSL context should be reloaded based on whether the
+     * {@link SSLOptions} have changed. This means that if the keystore certificate file changed but the file
+     * name remains the same, the SSL context is not considered to have been changed.
+     *
+     * <p>This class allows for us to keep track of the last modified timestamp of the underlying file, and if
+     * the underlying file changes, we propagate that information to the {@link SSLOptions} via the equality method in
+     * this class. When the old timestamp and new timestamp differ, we'll force the SSL context reloading in vertx.
+     */
+    static class WrappedKeyCertOptions implements KeyCertOptions
+    {
+        private final long timestamp;
+        private final KeyCertOptions delegate;
+
+        WrappedKeyCertOptions(long timestamp, KeyCertOptions delegate)
+        {
+            this.timestamp = timestamp;
+            this.delegate = delegate;
+        }
+
+        @Override
+        public KeyCertOptions copy()
+        {
+            return new WrappedKeyCertOptions(timestamp, delegate.copy());
+        }
+
+        @Override
+        public KeyManagerFactory getKeyManagerFactory(Vertx vertx) throws Exception
+        {
+            return delegate.getKeyManagerFactory(vertx);
+        }
+
+        @Override
+        public Function<String, X509KeyManager> keyManagerMapper(Vertx vertx) throws Exception
+        {
+            return delegate.keyManagerMapper(vertx);
+        }
+
+        @Override
+        public Function<String, KeyManagerFactory> keyManagerFactoryMapper(Vertx vertx) throws Exception
+        {
+            return delegate.keyManagerFactoryMapper(vertx);
+        }
+
+        @Override
+        public boolean equals(Object o)
+        {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            WrappedKeyCertOptions that = (WrappedKeyCertOptions) o;
+            return timestamp == that.timestamp
+                   && Objects.equals(delegate, that.delegate);
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return Objects.hash(timestamp, delegate);
         }
     }
 }
diff --git a/src/test/integration/org/apache/cassandra/sidecar/IntegrationTestBase.java b/src/test/integration/org/apache/cassandra/sidecar/IntegrationTestBase.java
index d1868c1..70bd110 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/IntegrationTestBase.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/IntegrationTestBase.java
@@ -43,16 +43,22 @@
 import com.google.inject.Injector;
 import com.google.inject.util.Modules;
 import io.vertx.core.Vertx;
-import io.vertx.core.http.HttpServer;
+import io.vertx.core.eventbus.Message;
+import io.vertx.core.eventbus.MessageConsumer;
+import io.vertx.core.json.JsonObject;
 import io.vertx.ext.web.client.WebClient;
 import io.vertx.junit5.VertxTestContext;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.cluster.InstancesConfig;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
 import org.apache.cassandra.sidecar.common.data.QualifiedTableName;
 import org.apache.cassandra.sidecar.common.dns.DnsResolver;
-import org.apache.cassandra.sidecar.testing.CassandraSidecarTestContext;
+import org.apache.cassandra.sidecar.server.MainModule;
+import org.apache.cassandra.sidecar.server.Server;
+import org.apache.cassandra.sidecar.test.CassandraSidecarTestContext;
 import org.apache.cassandra.testing.AbstractCassandraTestContext;
 
+import static org.apache.cassandra.sidecar.server.SidecarServerEvents.ON_CASSANDRA_CQL_READY;
 import static org.assertj.core.api.Assertions.assertThat;
 
 /**
@@ -64,8 +70,7 @@
 {
     protected Logger logger = LoggerFactory.getLogger(this.getClass());
     protected Vertx vertx;
-    protected HttpServer server;
-    protected InstancesConfig instancesConfig;
+    protected Server server;
 
     protected static final String TEST_KEYSPACE = "testkeyspace";
     private static final String TEST_TABLE_PREFIX = "testtable";
@@ -77,23 +82,34 @@
     @BeforeEach
     void setup(AbstractCassandraTestContext cassandraTestContext) throws InterruptedException
     {
-        sidecarTestContext = CassandraSidecarTestContext.from(cassandraTestContext, DnsResolver.DEFAULT);
-        Injector injector = Guice.createInjector(Modules
-                                                 .override(new MainModule())
-                                                 .with(new IntegrationTestModule(this.sidecarTestContext)));
-        instancesConfig = injector.getInstance(InstancesConfig.class);
-        server = injector.getInstance(HttpServer.class);
+        IntegrationTestModule integrationTestModule = new IntegrationTestModule();
+        Injector injector = Guice.createInjector(Modules.override(new MainModule()).with(integrationTestModule));
         vertx = injector.getInstance(Vertx.class);
+        sidecarTestContext = CassandraSidecarTestContext.from(vertx, cassandraTestContext, DnsResolver.DEFAULT);
+        integrationTestModule.setCassandraTestContext(sidecarTestContext);
+
+        server = injector.getInstance(Server.class);
 
         VertxTestContext context = new VertxTestContext();
-        server.listen(server.actualPort(), "127.0.0.1", context.succeeding(p -> {
-            if (sidecarTestContext.isClusterBuilt())
-            {
-                healthCheck(instancesConfig);
-            }
-            sidecarTestContext.registerInstanceConfigListener(instances -> healthCheck(instances));
-            context.completeNow();
-        }));
+
+        if (sidecarTestContext.isClusterBuilt())
+        {
+            MessageConsumer<Object> cqlReadyConsumer = vertx.eventBus().localConsumer(ON_CASSANDRA_CQL_READY.address());
+            cqlReadyConsumer.handler(message -> {
+                cqlReadyConsumer.unregister();
+                context.completeNow();
+            });
+        }
+
+        server.start()
+              .onSuccess(s -> {
+                  sidecarTestContext.registerInstanceConfigListener(this::healthCheck);
+                  if (!sidecarTestContext.isClusterBuilt())
+                  {
+                      context.completeNow();
+                  }
+              })
+              .onFailure(context::failNow);
 
         context.awaitCompletion(5, TimeUnit.SECONDS);
     }
@@ -101,9 +117,8 @@
     @AfterEach
     void tearDown() throws InterruptedException
     {
-        final CountDownLatch closeLatch = new CountDownLatch(1);
-        server.close(res -> closeLatch.countDown());
-        vertx.close();
+        CountDownLatch closeLatch = new CountDownLatch(1);
+        server.close().onSuccess(res -> closeLatch.countDown());
         if (closeLatch.await(60, TimeUnit.SECONDS))
             logger.info("Close event received before timeout.");
         else
@@ -114,8 +129,23 @@
     protected void testWithClient(VertxTestContext context, Consumer<WebClient> tester) throws Exception
     {
         WebClient client = WebClient.create(vertx);
+        CassandraAdapterDelegate delegate = sidecarTestContext.instancesConfig()
+                                                              .instanceFromId(1)
+                                                              .delegate();
 
-        tester.accept(client);
+        if (delegate.isUp())
+        {
+            tester.accept(client);
+        }
+        else
+        {
+            vertx.eventBus().localConsumer(ON_CASSANDRA_CQL_READY.address(), (Message<JsonObject> message) -> {
+                if (message.body().getInteger("cassandraInstanceId") == 1)
+                {
+                    tester.accept(client);
+                }
+            });
+        }
 
         // wait until the test completes
         assertThat(context.awaitCompletion(2, TimeUnit.MINUTES)).isTrue();
diff --git a/src/test/integration/org/apache/cassandra/sidecar/IntegrationTestModule.java b/src/test/integration/org/apache/cassandra/sidecar/IntegrationTestModule.java
index 523609d..7882237 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/IntegrationTestModule.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/IntegrationTestModule.java
@@ -27,20 +27,22 @@
 import com.google.inject.Singleton;
 import org.apache.cassandra.sidecar.cluster.InstancesConfig;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.config.HealthCheckConfiguration;
 import org.apache.cassandra.sidecar.config.ServiceConfiguration;
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.config.yaml.HealthCheckConfigurationImpl;
 import org.apache.cassandra.sidecar.config.yaml.ServiceConfigurationImpl;
 import org.apache.cassandra.sidecar.config.yaml.SidecarConfigurationImpl;
-import org.apache.cassandra.sidecar.testing.CassandraSidecarTestContext;
+import org.apache.cassandra.sidecar.test.CassandraSidecarTestContext;
 
 /**
  * Provides the basic dependencies for integration tests
  */
 public class IntegrationTestModule extends AbstractModule
 {
-    private final CassandraSidecarTestContext cassandraTestContext;
+    private CassandraSidecarTestContext cassandraTestContext;
 
-    public IntegrationTestModule(CassandraSidecarTestContext cassandraTestContext)
+    public void setCassandraTestContext(CassandraSidecarTestContext cassandraTestContext)
     {
         this.cassandraTestContext = cassandraTestContext;
     }
@@ -49,24 +51,17 @@
     @Singleton
     public InstancesConfig instancesConfig()
     {
-        return new WrapperInstancesConfig(cassandraTestContext);
+        return new WrapperInstancesConfig();
     }
 
-    static class WrapperInstancesConfig implements InstancesConfig
+    class WrapperInstancesConfig implements InstancesConfig
     {
-        private final CassandraSidecarTestContext cassandraTestContext;
-
-        WrapperInstancesConfig(CassandraSidecarTestContext cassandraTestContext)
-        {
-            this.cassandraTestContext = cassandraTestContext;
-        }
-
         /**
          * @return metadata of instances owned by the sidecar
          */
         public List<InstanceMetadata> instances()
         {
-            if (cassandraTestContext.isClusterBuilt())
+            if (cassandraTestContext != null && cassandraTestContext.isClusterBuilt())
                 return cassandraTestContext.instancesConfig().instances();
             return Collections.emptyList();
         }
@@ -100,7 +95,14 @@
     @Singleton
     public SidecarConfiguration configuration()
     {
-        ServiceConfiguration serviceConfiguration = new ServiceConfigurationImpl("127.0.0.1");
-        return new SidecarConfigurationImpl(serviceConfiguration);
+        ServiceConfiguration conf = ServiceConfigurationImpl.builder()
+                                                            .host("127.0.0.1")
+                                                            .port(0) // let the test find an available port
+                                                            .build();
+        HealthCheckConfiguration healthCheckConfiguration = new HealthCheckConfigurationImpl(50, 500);
+        return SidecarConfigurationImpl.builder()
+                                       .serviceConfiguration(conf)
+                                       .healthCheckConfiguration(healthCheckConfiguration)
+                                       .build();
     }
 }
diff --git a/src/test/integration/org/apache/cassandra/sidecar/common/DelegateTest.java b/src/test/integration/org/apache/cassandra/sidecar/common/DelegateTest.java
index e3665cf..25ad96d 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/common/DelegateTest.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/common/DelegateTest.java
@@ -18,24 +18,42 @@
 
 package org.apache.cassandra.sidecar.common;
 
-import java.util.concurrent.TimeUnit;
+import java.util.Set;
+import java.util.stream.IntStream;
 
-import com.datastax.driver.core.exceptions.TransportException;
+import com.google.common.collect.ImmutableSet;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import io.vertx.core.eventbus.EventBus;
+import io.vertx.core.eventbus.Message;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import io.vertx.junit5.Checkpoint;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
 import org.apache.cassandra.distributed.api.NodeToolResult;
 import org.apache.cassandra.sidecar.IntegrationTestBase;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.utils.SimpleCassandraVersion;
 import org.apache.cassandra.testing.CassandraIntegrationTest;
 
+import static org.apache.cassandra.sidecar.server.SidecarServerEvents.ON_ALL_CASSANDRA_CQL_READY;
+import static org.apache.cassandra.sidecar.server.SidecarServerEvents.ON_CASSANDRA_CQL_DISCONNECTED;
+import static org.apache.cassandra.sidecar.server.SidecarServerEvents.ON_CASSANDRA_CQL_READY;
 import static org.assertj.core.api.Assertions.assertThat;
 
 /**
  * Ensures the Delegate works correctly
  */
+@ExtendWith(VertxExtension.class)
 class DelegateTest extends IntegrationTestBase
 {
     @CassandraIntegrationTest(jmx = false)
     void testCorrectVersionIsEnabled()
     {
-        CassandraAdapterDelegate delegate = sidecarTestContext.instancesConfig().instances().get(0).delegate();
+        CassandraAdapterDelegate delegate = sidecarTestContext.instancesConfig()
+                                                              .instanceFromId(1)
+                                                              .delegate();
         SimpleCassandraVersion version = delegate.version();
         assertThat(version).isNotNull();
         assertThat(version.major).isEqualTo(sidecarTestContext.version.major);
@@ -44,39 +62,60 @@
     }
 
     @CassandraIntegrationTest(jmx = false)
-    void testHealthCheck() throws InterruptedException
+    void testHealthCheck(VertxTestContext context)
     {
-        CassandraAdapterDelegate delegate = sidecarTestContext.instancesConfig().instances().get(0).delegate();
+        EventBus eventBus = vertx.eventBus();
+        Checkpoint cqlReady = context.checkpoint();
+        Checkpoint cqlDisconnected = context.checkpoint();
 
-        delegate.healthCheck();
+        CassandraAdapterDelegate adapterDelegate = sidecarTestContext.instancesConfig()
+                                                                     .instanceFromId(1)
+                                                                     .delegate();
+        assertThat(adapterDelegate.isUp()).as("health check succeeds").isTrue();
 
-        assertThat(delegate.isUp()).as("health check succeeds").isTrue();
-
+        // Disable binary
         NodeToolResult nodetoolResult = sidecarTestContext.cluster().get(1).nodetoolResult("disablebinary");
         assertThat(nodetoolResult.getRc())
         .withFailMessage("Failed to disable binary:\nstdout:" + nodetoolResult.getStdout()
                          + "\nstderr: " + nodetoolResult.getStderr())
         .isEqualTo(0);
 
-        for (int i = 0; i < 10; i++)
-        {
-            try
-            {
-                delegate.healthCheck();
-                break;
-            }
-            catch (TransportException tex)
-            {
-                Thread.sleep(1000); // Give the delegate some time to recover
-            }
-        }
-        assertThat(delegate.isUp()).as("health check fails after binary has been disabled").isFalse();
+        eventBus.localConsumer(ON_CASSANDRA_CQL_DISCONNECTED.address(), (Message<JsonObject> message) -> {
+            int instanceId = message.body().getInteger("cassandraInstanceId");
+            CassandraAdapterDelegate delegate = sidecarTestContext.instancesConfig()
+                                                                  .instanceFromId(instanceId)
+                                                                  .delegate();
 
-        sidecarTestContext.cluster().get(1).nodetool("enablebinary");
+            assertThat(delegate.isUp()).as("health check fails after binary has been disabled").isFalse();
+            cqlDisconnected.flag();
+            sidecarTestContext.cluster().get(1).nodetool("enablebinary");
+        });
 
-        TimeUnit.SECONDS.sleep(1);
-        delegate.healthCheck();
+        eventBus.localConsumer(ON_CASSANDRA_CQL_READY.address(), (Message<JsonObject> reconnectMessage) -> {
+            int instanceId = reconnectMessage.body().getInteger("cassandraInstanceId");
+            CassandraAdapterDelegate delegate = sidecarTestContext.instancesConfig()
+                                                                  .instanceFromId(instanceId)
+                                                                  .delegate();
+            assertThat(delegate.isUp()).as("health check succeeds after binary has been enabled")
+                                       .isTrue();
+            cqlReady.flag();
+        });
+    }
 
-        assertThat(delegate.isUp()).as("health check succeeds after binary has been enabled").isTrue();
+    @CassandraIntegrationTest(jmx = false, nodesPerDc = 3)
+    void testAllInstancesHealthCheck(VertxTestContext context)
+    {
+        EventBus eventBus = vertx.eventBus();
+        Checkpoint allCqlReady = context.checkpoint();
+
+        Set<Integer> expectedCassandraInstanceIds = ImmutableSet.of(1, 2, 3);
+        eventBus.localConsumer(ON_ALL_CASSANDRA_CQL_READY.address(), (Message<JsonObject> message) -> {
+            JsonArray cassandraInstanceIds = message.body().getJsonArray("cassandraInstanceIds");
+            assertThat(cassandraInstanceIds).hasSize(3);
+            assertThat(IntStream.rangeClosed(1, cassandraInstanceIds.size()))
+            .allMatch(expectedCassandraInstanceIds::contains);
+
+            allCqlReady.flag();
+        });
     }
 }
diff --git a/src/test/integration/org/apache/cassandra/sidecar/common/JmxClientTest.java b/src/test/integration/org/apache/cassandra/sidecar/common/JmxClientTest.java
index 7295e44..e643918 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/common/JmxClientTest.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/common/JmxClientTest.java
@@ -103,6 +103,9 @@
     {
         IUpgradeableInstance instance = context.getCluster().getFirstRunningInstance();
         IInstanceConfig config = instance.config();
-        return new JmxClient(config.broadcastAddress().getAddress().getHostAddress(), config.jmxPort());
+        return JmxClient.builder()
+                        .host(config.broadcastAddress().getAddress().getHostAddress())
+                        .port(config.jmxPort())
+                        .build();
     }
 }
diff --git a/src/test/integration/org/apache/cassandra/sidecar/routes/GossipInfoHandlerIntegrationTest.java b/src/test/integration/org/apache/cassandra/sidecar/routes/GossipInfoHandlerIntegrationTest.java
index 9fd8fb1..de7c793 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/routes/GossipInfoHandlerIntegrationTest.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/routes/GossipInfoHandlerIntegrationTest.java
@@ -53,7 +53,7 @@
                       assertThat(gossipInfo.heartbeat()).isNotNull();
                       assertThat(gossipInfo.hostId()).isNotNull();
                       String releaseVersion = cassandraTestContext.getCluster().getFirstRunningInstance()
-                                                                          .getReleaseVersionString();
+                                                                  .getReleaseVersionString();
                       assertThat(gossipInfo.releaseVersion()).startsWith(releaseVersion);
                       context.completeNow();
                   }));
diff --git a/src/test/integration/org/apache/cassandra/sidecar/routes/RingHandlerIntegrationTest.java b/src/test/integration/org/apache/cassandra/sidecar/routes/RingHandlerIntegrationTest.java
index 38799b6..e52ba30 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/routes/RingHandlerIntegrationTest.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/routes/RingHandlerIntegrationTest.java
@@ -34,7 +34,7 @@
 import org.apache.cassandra.sidecar.IntegrationTestBase;
 import org.apache.cassandra.sidecar.common.data.RingEntry;
 import org.apache.cassandra.sidecar.common.data.RingResponse;
-import org.apache.cassandra.sidecar.testing.CassandraSidecarTestContext;
+import org.apache.cassandra.sidecar.test.CassandraSidecarTestContext;
 import org.apache.cassandra.testing.CassandraIntegrationTest;
 
 import static org.assertj.core.api.Assertions.assertThat;
diff --git a/src/test/integration/org/apache/cassandra/sidecar/routes/SnapshotsHandlerIntegrationTest.java b/src/test/integration/org/apache/cassandra/sidecar/routes/SnapshotsHandlerIntegrationTest.java
index c7f7203..424c179 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/routes/SnapshotsHandlerIntegrationTest.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/routes/SnapshotsHandlerIntegrationTest.java
@@ -32,7 +32,7 @@
 import io.vertx.junit5.VertxTestContext;
 import org.apache.cassandra.sidecar.IntegrationTestBase;
 import org.apache.cassandra.sidecar.common.data.QualifiedTableName;
-import org.apache.cassandra.sidecar.testing.CassandraSidecarTestContext;
+import org.apache.cassandra.sidecar.test.CassandraSidecarTestContext;
 import org.apache.cassandra.testing.CassandraIntegrationTest;
 
 import static io.netty.handler.codec.http.HttpResponseStatus.OK;
diff --git a/src/test/integration/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandlerIntegrationTest.java b/src/test/integration/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandlerIntegrationTest.java
index a53329e..b37099f 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandlerIntegrationTest.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandlerIntegrationTest.java
@@ -41,7 +41,7 @@
 import org.apache.cassandra.sidecar.IntegrationTestBase;
 import org.apache.cassandra.sidecar.common.data.ListSnapshotFilesResponse;
 import org.apache.cassandra.sidecar.common.data.QualifiedTableName;
-import org.apache.cassandra.sidecar.testing.CassandraSidecarTestContext;
+import org.apache.cassandra.sidecar.test.CassandraSidecarTestContext;
 import org.apache.cassandra.testing.CassandraIntegrationTest;
 
 import static io.netty.handler.codec.http.HttpResponseStatus.OK;
diff --git a/src/test/integration/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableImportHandlerIntegrationTest.java b/src/test/integration/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableImportHandlerIntegrationTest.java
index 628c184..3b7227b 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableImportHandlerIntegrationTest.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableImportHandlerIntegrationTest.java
@@ -46,9 +46,9 @@
 import io.vertx.junit5.VertxTestContext;
 import org.apache.cassandra.distributed.UpgradeableCluster;
 import org.apache.cassandra.sidecar.IntegrationTestBase;
-import org.apache.cassandra.sidecar.common.SimpleCassandraVersion;
 import org.apache.cassandra.sidecar.common.data.QualifiedTableName;
-import org.apache.cassandra.sidecar.testing.CassandraSidecarTestContext;
+import org.apache.cassandra.sidecar.test.CassandraSidecarTestContext;
+import org.apache.cassandra.sidecar.utils.SimpleCassandraVersion;
 import org.apache.cassandra.testing.CassandraIntegrationTest;
 
 import static org.assertj.core.api.Assertions.assertThat;
diff --git a/src/test/integration/org/apache/cassandra/sidecar/routes/tokenrange/BaseTokenRangeIntegrationTest.java b/src/test/integration/org/apache/cassandra/sidecar/routes/tokenrange/BaseTokenRangeIntegrationTest.java
index 1c046b6..7ebf902 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/routes/tokenrange/BaseTokenRangeIntegrationTest.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/routes/tokenrange/BaseTokenRangeIntegrationTest.java
@@ -267,10 +267,8 @@
                                      Handler<HttpResponse<Buffer>> verifier) throws Exception
     {
         String testRoute = "/api/v1/keyspaces/" + keyspace + "/token-range-replicas";
-        testWithClient(context, client -> {
-            client.get(server.actualPort(), "127.0.0.1", testRoute)
-                  .send(context.succeeding(verifier));
-        });
+        testWithClient(context, client -> client.get(server.actualPort(), "127.0.0.1", testRoute)
+                                            .send(context.succeeding(verifier)));
     }
 
     void assertMappingResponseOK(TokenRangeReplicasResponse mappingResponse,
diff --git a/src/test/integration/org/apache/cassandra/sidecar/routes/tokenrange/JoiningTest.java b/src/test/integration/org/apache/cassandra/sidecar/routes/tokenrange/JoiningTest.java
index 49335ac..5059378 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/routes/tokenrange/JoiningTest.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/routes/tokenrange/JoiningTest.java
@@ -59,8 +59,8 @@
         ClusterUtils.awaitRingState(instance, newInstance, "Normal");
 
         retrieveMappingWithKeyspace(context, TEST_KEYSPACE, response -> {
-            TokenRangeReplicasResponse mappingResponse = response.bodyAsJson(TokenRangeReplicasResponse.class);
             assertThat(response.statusCode()).isEqualTo(HttpResponseStatus.OK.code());
+            TokenRangeReplicasResponse mappingResponse = response.bodyAsJson(TokenRangeReplicasResponse.class);
             assertMappingResponseOK(mappingResponse, DEFAULT_RF, Collections.singleton("datacenter1"));
             context.completeNow();
         });
diff --git a/src/test/integration/org/apache/cassandra/sidecar/routes/tokenrange/ReplacementMultiDCTest.java b/src/test/integration/org/apache/cassandra/sidecar/routes/tokenrange/ReplacementMultiDCTest.java
index 30d4c42..a76df99 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/routes/tokenrange/ReplacementMultiDCTest.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/routes/tokenrange/ReplacementMultiDCTest.java
@@ -64,8 +64,7 @@
     throws Exception
     {
         BBHelperReplacementsMultiDC.reset();
-        UpgradeableCluster cluster = getMultiDCCluster(BBHelperReplacementsMultiDC::install, cassandraTestContext,
-                                                       builder -> builder.withDynamicPortAllocation(false));
+        UpgradeableCluster cluster = getMultiDCCluster(BBHelperReplacementsMultiDC::install, cassandraTestContext);
 
         List<IUpgradeableInstance> nodesToRemove = Arrays.asList(cluster.get(3), cluster.get(cluster.size()));
         runReplacementTestScenario(context,
diff --git a/src/test/integration/org/apache/cassandra/sidecar/testing/CassandraSidecarTestContext.java b/src/test/integration/org/apache/cassandra/sidecar/test/CassandraSidecarTestContext.java
similarity index 86%
rename from src/test/integration/org/apache/cassandra/sidecar/testing/CassandraSidecarTestContext.java
rename to src/test/integration/org/apache/cassandra/sidecar/test/CassandraSidecarTestContext.java
index 5243615..0bfab3b 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/testing/CassandraSidecarTestContext.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/test/CassandraSidecarTestContext.java
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.sidecar.testing;
+package org.apache.cassandra.sidecar.test;
 
 import java.net.InetSocketAddress;
 import java.nio.file.Path;
@@ -27,22 +27,23 @@
 
 import com.datastax.driver.core.NettyOptions;
 import com.datastax.driver.core.Session;
+import io.vertx.core.Vertx;
 import org.apache.cassandra.distributed.UpgradeableCluster;
 import org.apache.cassandra.distributed.api.IInstanceConfig;
 import org.apache.cassandra.distributed.api.IUpgradeableInstance;
 import org.apache.cassandra.distributed.shared.JMXUtil;
 import org.apache.cassandra.sidecar.adapters.base.CassandraFactory;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.cluster.InstancesConfig;
 import org.apache.cassandra.sidecar.cluster.InstancesConfigImpl;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadataImpl;
 import org.apache.cassandra.sidecar.common.CQLSessionProvider;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
-import org.apache.cassandra.sidecar.common.CassandraVersionProvider;
 import org.apache.cassandra.sidecar.common.JmxClient;
-import org.apache.cassandra.sidecar.common.SimpleCassandraVersion;
 import org.apache.cassandra.sidecar.common.dns.DnsResolver;
 import org.apache.cassandra.sidecar.common.utils.SidecarVersionProvider;
+import org.apache.cassandra.sidecar.utils.CassandraVersionProvider;
+import org.apache.cassandra.sidecar.utils.SimpleCassandraVersion;
 import org.apache.cassandra.testing.AbstractCassandraTestContext;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -56,17 +57,20 @@
     private final CassandraVersionProvider versionProvider;
     private final DnsResolver dnsResolver;
     private final AbstractCassandraTestContext abstractCassandraTestContext;
+    private final Vertx vertx;
     public InstancesConfig instancesConfig;
     private List<CQLSessionProvider> sessionProviders;
     private List<JmxClient> jmxClients;
     private static final SidecarVersionProvider svp = new SidecarVersionProvider("/sidecar.version");
     private final List<InstanceConfigListener> instanceConfigListeners;
 
-    private CassandraSidecarTestContext(AbstractCassandraTestContext abstractCassandraTestContext,
+    private CassandraSidecarTestContext(Vertx vertx,
+                                        AbstractCassandraTestContext abstractCassandraTestContext,
                                         SimpleCassandraVersion version,
                                         CassandraVersionProvider versionProvider,
                                         DnsResolver dnsResolver)
     {
+        this.vertx = vertx;
         this.instanceConfigListeners = new ArrayList<>();
         this.abstractCassandraTestContext = abstractCassandraTestContext;
         this.version = version;
@@ -74,7 +78,8 @@
         this.dnsResolver = dnsResolver;
     }
 
-    public static CassandraSidecarTestContext from(AbstractCassandraTestContext cassandraTestContext,
+    public static CassandraSidecarTestContext from(Vertx vertx,
+                                                   AbstractCassandraTestContext cassandraTestContext,
                                                    DnsResolver dnsResolver)
     {
         org.apache.cassandra.testing.SimpleCassandraVersion rootVersion = cassandraTestContext.version;
@@ -82,7 +87,8 @@
                                                                              rootVersion.minor,
                                                                              rootVersion.patch);
         CassandraVersionProvider versionProvider = cassandraVersionProvider(dnsResolver);
-        return new CassandraSidecarTestContext(cassandraTestContext,
+        return new CassandraSidecarTestContext(vertx,
+                                               cassandraTestContext,
                                                versionParsed,
                                                versionProvider,
                                                dnsResolver);
@@ -142,7 +148,9 @@
         {
             setInstancesConfig();
         }
-        return this.sessionProviders.get(instance).localCql();
+        CQLSessionProvider cqlSessionProvider = sessionProviders.get(instance);
+        assertThat(cqlSessionProvider).as("cqlSessionProvider for instance=" + instance).isNotNull();
+        return cqlSessionProvider.localCql();
     }
 
     @Override
@@ -217,7 +225,12 @@
             this.sessionProviders.add(sessionProvider);
             // The in-jvm dtest framework sometimes returns a cluster before all the jmx infrastructure is initialized.
             // In these cases, we want to wait longer than the default retry/delay settings to connect.
-            JmxClient jmxClient = new JmxClient(hostName, config.jmxPort(), null, null, false, 20, 1000L);
+            JmxClient jmxClient = JmxClient.builder()
+                                           .host(hostName)
+                                           .port(config.jmxPort())
+                                           .connectionMaxRetries(20)
+                                           .connectionRetryDelayMillis(1000L)
+                                           .build();
             this.jmxClients.add(jmxClient);
 
             String[] dataDirectories = (String[]) config.get("data_file_directories");
@@ -227,7 +240,9 @@
             assertThat(dataDirParentPath).isNotNull();
             Path stagingPath = dataDirParentPath.resolve("staging");
             String stagingDir = stagingPath.toFile().getAbsolutePath();
-            CassandraAdapterDelegate delegate = new CassandraAdapterDelegate(versionProvider,
+            CassandraAdapterDelegate delegate = new CassandraAdapterDelegate(vertx,
+                                                                             i + 1,
+                                                                             versionProvider,
                                                                              sessionProvider,
                                                                              jmxClient,
                                                                              "1.0-TEST");
diff --git a/src/test/integration/org/apache/cassandra/testing/AbstractCassandraTestContext.java b/src/test/integration/org/apache/cassandra/testing/AbstractCassandraTestContext.java
index fb33696..57519ca 100644
--- a/src/test/integration/org/apache/cassandra/testing/AbstractCassandraTestContext.java
+++ b/src/test/integration/org/apache/cassandra/testing/AbstractCassandraTestContext.java
@@ -62,6 +62,7 @@
     {
         if (cluster != null)
         {
+            LOGGER.info("Closing cluster={}", cluster);
             try
             {
                 cluster.close();
diff --git a/src/test/integration/org/apache/cassandra/testing/CassandraTestTemplate.java b/src/test/integration/org/apache/cassandra/testing/CassandraTestTemplate.java
index e07b84d..94683ba 100644
--- a/src/test/integration/org/apache/cassandra/testing/CassandraTestTemplate.java
+++ b/src/test/integration/org/apache/cassandra/testing/CassandraTestTemplate.java
@@ -25,7 +25,7 @@
 import java.util.Optional;
 import java.util.stream.Stream;
 
-import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
+import org.junit.jupiter.api.extension.AfterEachCallback;
 import org.junit.jupiter.api.extension.BeforeEachCallback;
 import org.junit.jupiter.api.extension.Extension;
 import org.junit.jupiter.api.extension.ExtensionContext;
@@ -137,7 +137,7 @@
         @Override
         public List<Extension> getAdditionalExtensions()
         {
-            return Arrays.asList(parameterResolver(), postProcessor(), beforeEach());
+            return Arrays.asList(parameterResolver(), afterEach(), beforeEach());
         }
 
         private BeforeEachCallback beforeEach()
@@ -189,11 +189,12 @@
         }
 
         /**
-         * Shuts down the in-jvm dtest cluster when the test is finished
+         * Shuts down the in-jvm dtest cluster after an individual test and any user-defined teardown methods
+         * have been executed
          *
-         * @return the {@link AfterTestExecutionCallback}
+         * @return the {@link AfterEachCallback}
          */
-        private AfterTestExecutionCallback postProcessor()
+        private AfterEachCallback afterEach()
         {
             return postProcessorCtx -> {
                 if (cassandraTestContext != null)
diff --git a/src/test/java/com/google/common/util/concurrent/SidecarRateLimiterTest.java b/src/test/java/com/google/common/util/concurrent/SidecarRateLimiterTest.java
new file mode 100644
index 0000000..afeb0a0
--- /dev/null
+++ b/src/test/java/com/google/common/util/concurrent/SidecarRateLimiterTest.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.common.util.concurrent;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for the {@link SidecarRateLimiter} class
+ */
+class SidecarRateLimiterTest
+{
+    @Test
+    void testCreation()
+    {
+        // Creates a SidecarRateLimiter that is enabled
+        SidecarRateLimiter enabledRateLimiter = SidecarRateLimiter.create(100);
+        assertThat(enabledRateLimiter).isNotNull();
+        assertThat(enabledRateLimiter.rate()).isEqualTo(100);
+        assertThat(enabledRateLimiter.tryAcquire()).isTrue();
+        enabledRateLimiter.rate(150);
+        assertThat(enabledRateLimiter.rate()).isEqualTo(150);
+        assertThat(enabledRateLimiter.queryEarliestAvailable(0)).isGreaterThan(0);
+
+        // Creates a SidecarRateLimiter that is disabled
+        SidecarRateLimiter disabledRateLimiter = SidecarRateLimiter.create(-1);
+        assertThat(disabledRateLimiter).isNotNull();
+        assertThat(disabledRateLimiter.rate()).isEqualTo(0);
+        assertThat(disabledRateLimiter.queryEarliestAvailable(1000L)).isEqualTo(0);
+    }
+
+    @Test
+    void testDisableRateLimitingBySettingRate()
+    {
+        SidecarRateLimiter rateLimiter = SidecarRateLimiter.create(1);
+        rateLimiter.acquire(2); // reserve some permits
+        assertThat(rateLimiter.tryAcquire()).isFalse();
+        assertThat(rateLimiter.acquire(2)).isGreaterThan(0.0);
+
+        rateLimiter.rate(-1);
+        assertThat(rateLimiter.tryAcquire()).isTrue();
+        rateLimiter.acquire(2000); // reserve some permits
+        assertThat(rateLimiter.acquire(2000)).isEqualTo(0.0);
+    }
+
+    @Test
+    void testEnableRateLimitingBySettingRate()
+    {
+        SidecarRateLimiter rateLimiter = SidecarRateLimiter.create(-1);
+        rateLimiter.acquire(2000); // reserve some permits
+        assertThat(rateLimiter.tryAcquire()).isTrue();
+        assertThat(rateLimiter.acquire(2000)).isEqualTo(0.0);
+        assertThat(rateLimiter.tryAcquire()).isTrue();
+
+        rateLimiter.rate(1);
+        rateLimiter.acquire(2); // reserve some permits
+        assertThat(rateLimiter.tryAcquire()).isFalse();
+        assertThat(rateLimiter.acquire(2)).isGreaterThan(0.0);
+    }
+
+    @Test
+    void testAcquireZeroPermitsDoesNotThrow()
+    {
+        SidecarRateLimiter rateLimiter = SidecarRateLimiter.create(100);
+        assertThat(rateLimiter.acquire(0)).isEqualTo(0);
+        assertThat(rateLimiter.acquire(5)).isEqualTo(0);
+        assertThat(rateLimiter.acquire(500)).isNotEqualTo(0);
+    }
+}
diff --git a/src/test/java/org/apache/cassandra/sidecar/AbstractHealthServiceTest.java b/src/test/java/org/apache/cassandra/sidecar/AbstractHealthServiceTest.java
index e56f477..bd86157 100644
--- a/src/test/java/org/apache/cassandra/sidecar/AbstractHealthServiceTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/AbstractHealthServiceTest.java
@@ -35,12 +35,13 @@
 import com.google.inject.Injector;
 import com.google.inject.util.Modules;
 import io.vertx.core.Vertx;
-import io.vertx.core.http.HttpServer;
 import io.vertx.core.net.JksOptions;
 import io.vertx.ext.web.client.WebClient;
 import io.vertx.ext.web.client.WebClientOptions;
 import io.vertx.ext.web.codec.BodyCodec;
 import io.vertx.junit5.VertxTestContext;
+import org.apache.cassandra.sidecar.server.MainModule;
+import org.apache.cassandra.sidecar.server.Server;
 
 import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
 import static io.netty.handler.codec.http.HttpResponseStatus.OK;
@@ -57,7 +58,7 @@
     @TempDir
     private Path certPath;
     private Vertx vertx;
-    private HttpServer server;
+    private Server server;
 
     public abstract boolean isSslEnabled();
 
@@ -73,11 +74,13 @@
     void setUp() throws InterruptedException
     {
         Injector injector = Guice.createInjector(Modules.override(new MainModule()).with(testModule()));
-        server = injector.getInstance(HttpServer.class);
+        server = injector.getInstance(Server.class);
         vertx = injector.getInstance(Vertx.class);
 
         VertxTestContext context = new VertxTestContext();
-        server.listen(0, context.succeedingThenComplete());
+        server.start()
+              .onSuccess(s -> context.completeNow())
+              .onFailure(context::failNow);
 
         context.awaitCompletion(5, TimeUnit.SECONDS);
     }
@@ -86,8 +89,7 @@
     void tearDown() throws InterruptedException
     {
         final CountDownLatch closeLatch = new CountDownLatch(1);
-        server.close(res -> closeLatch.countDown());
-        vertx.close();
+        server.close().onSuccess(res -> closeLatch.countDown());
         if (closeLatch.await(60, TimeUnit.SECONDS))
             logger.info("Close event received before timeout.");
         else
diff --git a/src/test/java/org/apache/cassandra/sidecar/LoggerHandlerInjectionTest.java b/src/test/java/org/apache/cassandra/sidecar/LoggerHandlerInjectionTest.java
index 50ae53e..41704a8 100644
--- a/src/test/java/org/apache/cassandra/sidecar/LoggerHandlerInjectionTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/LoggerHandlerInjectionTest.java
@@ -35,7 +35,6 @@
 import io.netty.handler.codec.http.HttpResponseStatus;
 import io.vertx.core.Handler;
 import io.vertx.core.Vertx;
-import io.vertx.core.http.HttpServer;
 import io.vertx.core.http.HttpServerRequest;
 import io.vertx.ext.web.Router;
 import io.vertx.ext.web.RoutingContext;
@@ -46,6 +45,8 @@
 import io.vertx.ext.web.handler.LoggerHandler;
 import io.vertx.junit5.VertxExtension;
 import io.vertx.junit5.VertxTestContext;
+import org.apache.cassandra.sidecar.server.MainModule;
+import org.apache.cassandra.sidecar.server.Server;
 
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
@@ -60,7 +61,7 @@
 {
     private Vertx vertx;
     private final Logger logger = mock(Logger.class);
-    private HttpServer server;
+    private Server server;
     private FakeLoggerHandler loggerHandler;
 
     @BeforeEach
@@ -77,8 +78,10 @@
         router.get("/fake-route").handler(promise -> promise.json("done"));
 
         VertxTestContext context = new VertxTestContext();
-        server = injector.getInstance(HttpServer.class);
-        server.listen(0, context.succeedingThenComplete());
+        server = injector.getInstance(Server.class);
+        server.start()
+              .onSuccess(s -> context.completeNow())
+              .onFailure(context::failNow);
 
         context.awaitCompletion(5, TimeUnit.SECONDS);
     }
@@ -86,9 +89,8 @@
     @AfterEach
     void tearDown() throws InterruptedException
     {
-        final CountDownLatch closeLatch = new CountDownLatch(1);
-        server.close(res -> closeLatch.countDown());
-        vertx.close();
+        CountDownLatch closeLatch = new CountDownLatch(1);
+        server.close().onSuccess(res -> closeLatch.countDown());
         if (closeLatch.await(60, TimeUnit.SECONDS))
         {
             logger.info("Close event received before timeout.");
diff --git a/src/test/java/org/apache/cassandra/sidecar/TestModule.java b/src/test/java/org/apache/cassandra/sidecar/TestModule.java
index 3e6ba6e..d78d11c 100644
--- a/src/test/java/org/apache/cassandra/sidecar/TestModule.java
+++ b/src/test/java/org/apache/cassandra/sidecar/TestModule.java
@@ -25,13 +25,10 @@
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
-import io.vertx.core.Vertx;
-import io.vertx.core.file.FileSystem;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.cluster.InstancesConfig;
 import org.apache.cassandra.sidecar.cluster.InstancesConfigImpl;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
-import org.apache.cassandra.sidecar.common.CassandraVersionProvider;
 import org.apache.cassandra.sidecar.common.MockCassandraFactory;
 import org.apache.cassandra.sidecar.common.NodeSettings;
 import org.apache.cassandra.sidecar.common.dns.DnsResolver;
@@ -42,11 +39,11 @@
 import org.apache.cassandra.sidecar.config.SslConfiguration;
 import org.apache.cassandra.sidecar.config.ThrottleConfiguration;
 import org.apache.cassandra.sidecar.config.yaml.HealthCheckConfigurationImpl;
-import org.apache.cassandra.sidecar.config.yaml.JmxConfigurationImpl;
 import org.apache.cassandra.sidecar.config.yaml.SSTableUploadConfigurationImpl;
 import org.apache.cassandra.sidecar.config.yaml.ServiceConfigurationImpl;
 import org.apache.cassandra.sidecar.config.yaml.SidecarConfigurationImpl;
 import org.apache.cassandra.sidecar.config.yaml.ThrottleConfigurationImpl;
+import org.apache.cassandra.sidecar.utils.CassandraVersionProvider;
 
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -79,12 +76,19 @@
     {
         ThrottleConfiguration throttleConfiguration = new ThrottleConfigurationImpl(1, 10, 5);
         SSTableUploadConfiguration uploadConfiguration = new SSTableUploadConfigurationImpl(0F);
-        ServiceConfiguration serviceConfiguration = new ServiceConfigurationImpl("127.0.0.1",
-                                                                                 throttleConfiguration,
-                                                                                 uploadConfiguration,
-                                                                                 new JmxConfigurationImpl());
-        HealthCheckConfiguration healthCheckConfiguration = new HealthCheckConfigurationImpl(1000);
-        return new SidecarConfigurationImpl(serviceConfiguration, sslConfiguration, healthCheckConfiguration);
+        ServiceConfiguration serviceConfiguration =
+        ServiceConfigurationImpl.builder()
+                                .host("127.0.0.1")
+                                .port(0) // let the test find an available port
+                                .throttleConfiguration(throttleConfiguration)
+                                .ssTableUploadConfiguration(uploadConfiguration)
+                                .build();
+        HealthCheckConfiguration healthCheckConfiguration = new HealthCheckConfigurationImpl(200, 1000);
+        return SidecarConfigurationImpl.builder()
+                                       .serviceConfiguration(serviceConfiguration)
+                                       .sslConfiguration(sslConfiguration)
+                                       .healthCheckConfiguration(healthCheckConfiguration)
+                                       .build();
     }
 
     @Provides
@@ -125,13 +129,6 @@
         return instanceMeta;
     }
 
-    @Provides
-    @Singleton
-    public FileSystem fileSystem(Vertx vertx)
-    {
-        return vertx.fileSystem();
-    }
-
     /**
      * The Mock factory is used for testing purposes, enabling us to test all failures and possible results
      *
diff --git a/src/test/java/org/apache/cassandra/sidecar/TestSslModule.java b/src/test/java/org/apache/cassandra/sidecar/TestSslModule.java
index 0539a6f..4660c0a 100644
--- a/src/test/java/org/apache/cassandra/sidecar/TestSslModule.java
+++ b/src/test/java/org/apache/cassandra/sidecar/TestSslModule.java
@@ -65,11 +65,16 @@
         }
 
         SslConfiguration sslConfiguration =
-        new SslConfigurationImpl(true,
-                                 new KeyStoreConfigurationImpl(keyStorePath.toAbsolutePath().toString(),
-                                                               keyStorePassword),
-                                 new KeyStoreConfigurationImpl(trustStorePath.toAbsolutePath().toString(),
-                                                               trustStorePassword));
+        SslConfigurationImpl.builder()
+                            .enabled(true)
+                            .useOpenSsl(true)
+                            .handshakeTimeoutInSeconds(10L)
+                            .clientAuth("NONE")
+                            .keystore(new KeyStoreConfigurationImpl(keyStorePath.toAbsolutePath().toString(),
+                                                                    keyStorePassword))
+                            .truststore(new KeyStoreConfigurationImpl(trustStorePath.toAbsolutePath().toString(),
+                                                                      trustStorePassword))
+                            .build();
 
         return super.abstractConfig(sslConfiguration);
     }
diff --git a/src/test/java/org/apache/cassandra/sidecar/ThrottleTest.java b/src/test/java/org/apache/cassandra/sidecar/ThrottleTest.java
index 26fe546..8077214 100644
--- a/src/test/java/org/apache/cassandra/sidecar/ThrottleTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/ThrottleTest.java
@@ -34,12 +34,13 @@
 import com.google.inject.util.Modules;
 import io.netty.handler.codec.http.HttpResponseStatus;
 import io.vertx.core.Vertx;
-import io.vertx.core.http.HttpServer;
 import io.vertx.ext.web.client.HttpResponse;
 import io.vertx.ext.web.client.WebClient;
 import io.vertx.ext.web.codec.BodyCodec;
 import io.vertx.junit5.VertxExtension;
 import io.vertx.junit5.VertxTestContext;
+import org.apache.cassandra.sidecar.server.MainModule;
+import org.apache.cassandra.sidecar.server.Server;
 
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -52,27 +53,27 @@
 {
     private static final Logger logger = LoggerFactory.getLogger(ThrottleTest.class);
     private Vertx vertx;
-    private HttpServer server;
+    private Server server;
 
     @BeforeEach
     void setUp() throws InterruptedException
     {
         Injector injector = Guice.createInjector(Modules.override(new MainModule()).with(new TestModule()));
-        server = injector.getInstance(HttpServer.class);
+        server = injector.getInstance(Server.class);
         vertx = injector.getInstance(Vertx.class);
 
         VertxTestContext context = new VertxTestContext();
-        server.listen(0, context.succeedingThenComplete());
-
+        server.start()
+              .onSuccess(s -> context.completeNow())
+              .onFailure(context::failNow);
         context.awaitCompletion(5, SECONDS);
     }
 
     @AfterEach
     void tearDown() throws InterruptedException
     {
-        final CountDownLatch closeLatch = new CountDownLatch(1);
-        server.close(res -> closeLatch.countDown());
-        vertx.close();
+        CountDownLatch closeLatch = new CountDownLatch(1);
+        server.close().onSuccess(res -> closeLatch.countDown());
         if (closeLatch.await(60, SECONDS))
             logger.info("Close event received before timeout.");
         else
diff --git a/src/test/java/org/apache/cassandra/sidecar/config/SidecarConfigurationTest.java b/src/test/java/org/apache/cassandra/sidecar/config/SidecarConfigurationTest.java
index 83161f9..36677df 100644
--- a/src/test/java/org/apache/cassandra/sidecar/config/SidecarConfigurationTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/config/SidecarConfigurationTest.java
@@ -26,6 +26,7 @@
 
 import com.fasterxml.jackson.databind.JsonMappingException;
 import org.apache.cassandra.sidecar.config.yaml.SidecarConfigurationImpl;
+import org.assertj.core.api.Condition;
 
 import static org.apache.cassandra.sidecar.common.ResourceUtils.writeResourceToPath;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -159,6 +160,17 @@
         .withMessageContaining("Invalid file_permissions configuration=\"not-valid\"");
     }
 
+    @Test
+    void testInvalidClientAuth()
+    {
+        Path yamlPath = yaml("config/sidecar_invalid_client_auth.yaml");
+        assertThatExceptionOfType(JsonMappingException.class)
+        .isThrownBy(() -> SidecarConfigurationImpl.readYamlConfiguration(yamlPath))
+        .withRootCauseInstanceOf(IllegalArgumentException.class)
+        .withMessageContaining("Invalid client_auth configuration=\"notvalid\", " +
+                               "valid values are (NONE,REQUEST,REQUIRED)");
+    }
+
     void validateSingleInstanceSidecarConfiguration(SidecarConfiguration config)
     {
         assertThat(config.cassandraInstances()).isNotNull().hasSize(1);
@@ -186,8 +198,7 @@
         assertThat(config.sslConfiguration()).isNull();
 
         // health check configuration
-        assertThat(config.healthCheckConfiguration()).isNotNull();
-        assertThat(config.healthCheckConfiguration().checkIntervalMillis()).isEqualTo(30_000);
+        validateHealthCheckConfiguration(config.healthCheckConfiguration());
 
         // cassandra input validation configuration
         validateDefaultCassandraInputValidationConfiguration(config.cassandraInputValidationConfiguration());
@@ -252,8 +263,7 @@
         }
 
         // health check configuration
-        assertThat(config.healthCheckConfiguration()).isNotNull();
-        assertThat(config.healthCheckConfiguration().checkIntervalMillis()).isEqualTo(30_000);
+        validateHealthCheckConfiguration(config.healthCheckConfiguration());
 
         // cassandra input validation configuration
         validateDefaultCassandraInputValidationConfiguration(config.cassandraInputValidationConfiguration());
@@ -263,10 +273,12 @@
     {
         assertThat(serviceConfiguration).isNotNull();
         assertThat(serviceConfiguration.host()).isEqualTo("0.0.0.0");
-        assertThat(serviceConfiguration.port()).isEqualTo(9043);
+        assertThat(serviceConfiguration.port()).is(new Condition<>(port -> port == 9043 || port == 0, "port"));
         assertThat(serviceConfiguration.requestIdleTimeoutMillis()).isEqualTo(300000);
         assertThat(serviceConfiguration.requestTimeoutMillis()).isEqualTo(300000);
         assertThat(serviceConfiguration.allowableSkewInMinutes()).isEqualTo(60);
+        assertThat(serviceConfiguration.tcpKeepAlive()).isFalse();
+        assertThat(serviceConfiguration.acceptBacklog()).isEqualTo(1024);
 
         // service configuration throttling
         ThrottleConfiguration throttle = serviceConfiguration.throttleConfiguration();
@@ -275,6 +287,22 @@
         assertThat(throttle.rateLimitStreamRequestsPerSecond()).isEqualTo(5000);
         assertThat(throttle.delayInSeconds()).isEqualTo(5);
         assertThat(throttle.timeoutInSeconds()).isEqualTo(10);
+
+        // validate traffic shaping options
+        TrafficShapingConfiguration trafficShaping = serviceConfiguration.trafficShapingConfiguration();
+        assertThat(trafficShaping).isNotNull();
+        assertThat(trafficShaping.inboundGlobalBandwidthBytesPerSecond()).isEqualTo(500L);
+        assertThat(trafficShaping.outboundGlobalBandwidthBytesPerSecond()).isEqualTo(1500L);
+        assertThat(trafficShaping.peakOutboundGlobalBandwidthBytesPerSecond()).isEqualTo(2000L);
+        assertThat(trafficShaping.maxDelayToWaitMillis()).isEqualTo(2500L);
+        assertThat(trafficShaping.checkIntervalForStatsMillis()).isEqualTo(3000L);
+    }
+
+    private void validateHealthCheckConfiguration(HealthCheckConfiguration config)
+    {
+        assertThat(config).isNotNull();
+        assertThat(config.initialDelayMillis()).isEqualTo(100);
+        assertThat(config.checkIntervalMillis()).isEqualTo(30_000);
     }
 
     void validateDefaultCassandraInputValidationConfiguration(CassandraInputValidationConfiguration config)
@@ -297,12 +325,19 @@
     {
         assertThat(config).isNotNull();
         assertThat(config.enabled()).isTrue();
+        assertThat(config.preferOpenSSL()).isFalse();
+        assertThat(config.handshakeTimeoutInSeconds()).isEqualTo(25L);
+        assertThat(config.clientAuth()).isEqualTo("REQUEST");
         assertThat(config.keystore()).isNotNull();
         assertThat(config.keystore().path()).isEqualTo("path/to/keystore.p12");
         assertThat(config.keystore().password()).isEqualTo("password");
+        assertThat(config.keystore().reloadStore()).isTrue();
+        assertThat(config.keystore().checkIntervalInSeconds()).isEqualTo(300);
         assertThat(config.truststore()).isNotNull();
         assertThat(config.truststore().path()).isEqualTo("path/to/truststore.p12");
         assertThat(config.truststore().password()).isEqualTo("password");
+        assertThat(config.truststore().reloadStore()).isFalse();
+        assertThat(config.truststore().checkIntervalInSeconds()).isEqualTo(-1);
     }
 
     private Path yaml(String resourceName)
diff --git a/src/test/java/org/apache/cassandra/sidecar/routes/GossipInfoHandlerTest.java b/src/test/java/org/apache/cassandra/sidecar/routes/GossipInfoHandlerTest.java
index 77b28d9..dd284a8 100644
--- a/src/test/java/org/apache/cassandra/sidecar/routes/GossipInfoHandlerTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/routes/GossipInfoHandlerTest.java
@@ -37,18 +37,18 @@
 import com.google.inject.Singleton;
 import com.google.inject.util.Modules;
 import io.vertx.core.Vertx;
-import io.vertx.core.http.HttpServer;
 import io.vertx.ext.web.client.WebClient;
 import io.vertx.ext.web.client.predicate.ResponsePredicate;
 import io.vertx.junit5.VertxExtension;
 import io.vertx.junit5.VertxTestContext;
-import org.apache.cassandra.sidecar.MainModule;
 import org.apache.cassandra.sidecar.TestModule;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.cluster.InstancesConfig;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.common.ClusterMembershipOperations;
 import org.apache.cassandra.sidecar.common.data.GossipInfoResponse;
+import org.apache.cassandra.sidecar.server.MainModule;
+import org.apache.cassandra.sidecar.server.Server;
 
 import static io.netty.handler.codec.http.HttpResponseStatus.OK;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -63,7 +63,7 @@
 {
     static final Logger LOGGER = LoggerFactory.getLogger(GossipInfoHandlerTest.class);
     Vertx vertx;
-    HttpServer server;
+    Server server;
 
     @BeforeEach
     void before() throws InterruptedException
@@ -74,18 +74,19 @@
         injector = Guice.createInjector(Modules.override(new MainModule())
                                                .with(testOverride));
         vertx = injector.getInstance(Vertx.class);
-        server = injector.getInstance(HttpServer.class);
+        server = injector.getInstance(Server.class);
         VertxTestContext context = new VertxTestContext();
-        server.listen(0, "127.0.0.1", context.succeedingThenComplete());
+        server.start()
+              .onSuccess(s -> context.completeNow())
+              .onFailure(context::failNow);
         context.awaitCompletion(5, TimeUnit.SECONDS);
     }
 
     @AfterEach
     void after() throws InterruptedException
     {
-        final CountDownLatch closeLatch = new CountDownLatch(1);
-        server.close(res -> closeLatch.countDown());
-        vertx.close();
+        CountDownLatch closeLatch = new CountDownLatch(1);
+        server.close().onSuccess(res -> closeLatch.countDown());
         if (closeLatch.await(60, TimeUnit.SECONDS))
             LOGGER.info("Close event received before timeout.");
         else
diff --git a/src/test/java/org/apache/cassandra/sidecar/routes/RingHandlerTest.java b/src/test/java/org/apache/cassandra/sidecar/routes/RingHandlerTest.java
index 73631e8..4afeaeb 100644
--- a/src/test/java/org/apache/cassandra/sidecar/routes/RingHandlerTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/routes/RingHandlerTest.java
@@ -41,21 +41,21 @@
 import com.google.inject.Singleton;
 import com.google.inject.util.Modules;
 import io.vertx.core.Vertx;
-import io.vertx.core.http.HttpServer;
 import io.vertx.core.json.JsonObject;
 import io.vertx.ext.web.client.WebClient;
 import io.vertx.ext.web.client.predicate.ResponsePredicate;
 import io.vertx.junit5.VertxExtension;
 import io.vertx.junit5.VertxTestContext;
-import org.apache.cassandra.sidecar.MainModule;
 import org.apache.cassandra.sidecar.TestModule;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.cluster.InstancesConfig;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.common.StorageOperations;
 import org.apache.cassandra.sidecar.common.data.RingEntry;
 import org.apache.cassandra.sidecar.common.data.RingResponse;
 import org.apache.cassandra.sidecar.common.exceptions.JmxAuthenticationException;
+import org.apache.cassandra.sidecar.server.MainModule;
+import org.apache.cassandra.sidecar.server.Server;
 import org.mockito.stubbing.Answer;
 
 import static io.netty.handler.codec.http.HttpResponseStatus.OK;
@@ -72,7 +72,7 @@
 {
     static final Logger LOGGER = LoggerFactory.getLogger(RingHandlerTest.class);
     Vertx vertx;
-    HttpServer server;
+    Server server;
 
     @BeforeEach
     void before() throws InterruptedException
@@ -82,18 +82,19 @@
         Injector injector = Guice.createInjector(Modules.override(new MainModule())
                                                         .with(testOverride));
         vertx = injector.getInstance(Vertx.class);
-        server = injector.getInstance(HttpServer.class);
+        server = injector.getInstance(Server.class);
         VertxTestContext context = new VertxTestContext();
-        server.listen(0, "127.0.0.1", context.succeedingThenComplete());
+        server.start()
+              .onSuccess(s -> context.completeNow())
+              .onFailure(context::failNow);
         context.awaitCompletion(5, TimeUnit.SECONDS);
     }
 
     @AfterEach
     void after() throws InterruptedException
     {
-        final CountDownLatch closeLatch = new CountDownLatch(1);
-        server.close(res -> closeLatch.countDown());
-        vertx.close();
+        CountDownLatch closeLatch = new CountDownLatch(1);
+        server.close().onSuccess(res -> closeLatch.countDown());
         if (closeLatch.await(60, TimeUnit.SECONDS))
             LOGGER.info("Close event received before timeout.");
         else
diff --git a/src/test/java/org/apache/cassandra/sidecar/routes/SchemaHandlerTest.java b/src/test/java/org/apache/cassandra/sidecar/routes/SchemaHandlerTest.java
index 82c6909..0a275a1 100644
--- a/src/test/java/org/apache/cassandra/sidecar/routes/SchemaHandlerTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/routes/SchemaHandlerTest.java
@@ -41,18 +41,18 @@
 import com.google.inject.Singleton;
 import com.google.inject.util.Modules;
 import io.vertx.core.Vertx;
-import io.vertx.core.http.HttpServer;
 import io.vertx.core.json.JsonObject;
 import io.vertx.ext.web.client.WebClient;
 import io.vertx.ext.web.client.predicate.ResponsePredicate;
 import io.vertx.junit5.VertxExtension;
 import io.vertx.junit5.VertxTestContext;
-import org.apache.cassandra.sidecar.MainModule;
 import org.apache.cassandra.sidecar.TestModule;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.cluster.InstancesConfig;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.common.utils.IOUtils;
+import org.apache.cassandra.sidecar.server.MainModule;
+import org.apache.cassandra.sidecar.server.Server;
 
 import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
 import static io.netty.handler.codec.http.HttpResponseStatus.OK;
@@ -68,7 +68,7 @@
 {
     static final Logger LOGGER = LoggerFactory.getLogger(SchemaHandlerTest.class);
     Vertx vertx;
-    HttpServer server;
+    Server server;
     @TempDir
     File dataDir0;
     String testKeyspaceSchema;
@@ -84,18 +84,19 @@
                                                .with(Modules.override(new TestModule())
                                                             .with(new SchemaHandlerTestModule())));
         vertx = injector.getInstance(Vertx.class);
-        server = injector.getInstance(HttpServer.class);
+        server = injector.getInstance(Server.class);
         VertxTestContext context = new VertxTestContext();
-        server.listen(0, "127.0.0.1", context.succeedingThenComplete());
+        server.start()
+              .onSuccess(s -> context.completeNow())
+              .onFailure(context::failNow);
         context.awaitCompletion(5, TimeUnit.SECONDS);
     }
 
     @AfterEach
     void after() throws InterruptedException
     {
-        final CountDownLatch closeLatch = new CountDownLatch(1);
-        server.close(res -> closeLatch.countDown());
-        vertx.close();
+        CountDownLatch closeLatch = new CountDownLatch(1);
+        server.close().onSuccess(res -> closeLatch.countDown());
         if (closeLatch.await(60, TimeUnit.SECONDS))
             LOGGER.info("Close event received before timeout.");
         else
diff --git a/src/test/java/org/apache/cassandra/sidecar/routes/SnapshotsHandlerTest.java b/src/test/java/org/apache/cassandra/sidecar/routes/SnapshotsHandlerTest.java
index 6d04f94..2f28e31 100644
--- a/src/test/java/org/apache/cassandra/sidecar/routes/SnapshotsHandlerTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/routes/SnapshotsHandlerTest.java
@@ -18,8 +18,8 @@
 
 package org.apache.cassandra.sidecar.routes;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
@@ -42,14 +42,14 @@
 import com.google.inject.Singleton;
 import com.google.inject.util.Modules;
 import io.vertx.core.Vertx;
-import io.vertx.core.http.HttpServer;
 import io.vertx.ext.web.client.WebClient;
 import io.vertx.junit5.VertxExtension;
 import io.vertx.junit5.VertxTestContext;
-import org.apache.cassandra.sidecar.MainModule;
 import org.apache.cassandra.sidecar.TestModule;
 import org.apache.cassandra.sidecar.cluster.InstancesConfig;
 import org.apache.cassandra.sidecar.common.data.ListSnapshotFilesResponse;
+import org.apache.cassandra.sidecar.server.MainModule;
+import org.apache.cassandra.sidecar.server.Server;
 import org.apache.cassandra.sidecar.snapshots.SnapshotUtils;
 
 import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
@@ -67,32 +67,35 @@
 {
     private static final Logger logger = LoggerFactory.getLogger(SnapshotsHandlerTest.class);
     private Vertx vertx;
-    private HttpServer server;
+    private Server server;
     @TempDir
-    File temporaryFolder;
+    Path temporaryPath;
+    String canonicalTemporaryPath;
 
     @BeforeEach
     public void setup() throws InterruptedException, IOException
     {
+        canonicalTemporaryPath = temporaryPath.toFile().getCanonicalPath();
         Injector injector = Guice.createInjector(Modules.override(new MainModule())
                                                         .with(Modules.override(new TestModule())
                                                                      .with(new ListSnapshotTestModule())));
-        server = injector.getInstance(HttpServer.class);
+        server = injector.getInstance(Server.class);
         vertx = injector.getInstance(Vertx.class);
 
         VertxTestContext context = new VertxTestContext();
-        server.listen(0, "localhost", context.succeedingThenComplete());
+        server.start()
+              .onSuccess(s -> context.completeNow())
+              .onFailure(context::failNow);
 
         context.awaitCompletion(5, TimeUnit.SECONDS);
-        SnapshotUtils.initializeTmpDirectory(temporaryFolder);
+        SnapshotUtils.initializeTmpDirectory(temporaryPath.toFile());
     }
 
     @AfterEach
     void tearDown() throws InterruptedException
     {
-        final CountDownLatch closeLatch = new CountDownLatch(1);
-        server.close(res -> closeLatch.countDown());
-        vertx.close();
+        CountDownLatch closeLatch = new CountDownLatch(1);
+        server.close().onSuccess(res -> closeLatch.countDown());
         if (closeLatch.await(60, TimeUnit.SECONDS))
             logger.info("Close event received before timeout.");
         else
@@ -107,7 +110,7 @@
         ListSnapshotFilesResponse.FileInfo fileInfoExpected =
         new ListSnapshotFilesResponse.FileInfo(11,
                                                "localhost",
-                                               9043,
+                                               0,
                                                0,
                                                "snapshot1",
                                                "keyspace1",
@@ -116,7 +119,7 @@
         ListSnapshotFilesResponse.FileInfo fileInfoNotExpected =
         new ListSnapshotFilesResponse.FileInfo(11,
                                                "localhost",
-                                               9043,
+                                               0,
                                                0,
                                                "snapshot1",
                                                "keyspace1",
@@ -143,7 +146,7 @@
         List<ListSnapshotFilesResponse.FileInfo> fileInfoExpected = Arrays.asList(
         new ListSnapshotFilesResponse.FileInfo(11,
                                                "localhost",
-                                               9043,
+                                               0,
                                                0,
                                                "snapshot1",
                                                "keyspace1",
@@ -151,7 +154,7 @@
                                                "1.db"),
         new ListSnapshotFilesResponse.FileInfo(0,
                                                "localhost",
-                                               9043,
+                                               0,
                                                0,
                                                "snapshot1",
                                                "keyspace1",
@@ -237,9 +240,9 @@
     {
         @Provides
         @Singleton
-        public InstancesConfig instancesConfig() throws IOException
+        public InstancesConfig instancesConfig(Vertx vertx)
         {
-            return mockInstancesConfig(temporaryFolder.getCanonicalPath());
+            return mockInstancesConfig(vertx, canonicalTemporaryPath);
         }
     }
 }
diff --git a/src/test/java/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandlerTest.java b/src/test/java/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandlerTest.java
index 0c41b40..48a0385 100644
--- a/src/test/java/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandlerTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/routes/StreamSSTableComponentHandlerTest.java
@@ -35,13 +35,13 @@
 import com.google.inject.util.Modules;
 import io.netty.handler.codec.http.HttpHeaderNames;
 import io.vertx.core.Vertx;
-import io.vertx.core.http.HttpServer;
 import io.vertx.ext.web.client.WebClient;
 import io.vertx.ext.web.codec.BodyCodec;
 import io.vertx.junit5.VertxExtension;
 import io.vertx.junit5.VertxTestContext;
-import org.apache.cassandra.sidecar.MainModule;
 import org.apache.cassandra.sidecar.TestModule;
+import org.apache.cassandra.sidecar.server.MainModule;
+import org.apache.cassandra.sidecar.server.Server;
 
 import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
 import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
@@ -62,17 +62,19 @@
     static final String TEST_TABLE = "TestTable-54ea95ce-bba2-4e0a-a9be-e428e5d7160b";
 
     private Vertx vertx;
-    private HttpServer server;
+    private Server server;
 
     @BeforeEach
     void setUp() throws InterruptedException
     {
         Injector injector = Guice.createInjector(Modules.override(new MainModule()).with(new TestModule()));
-        server = injector.getInstance(HttpServer.class);
+        server = injector.getInstance(Server.class);
         vertx = injector.getInstance(Vertx.class);
 
         VertxTestContext context = new VertxTestContext();
-        server.listen(0, "localhost", context.succeedingThenComplete());
+        server.start()
+              .onSuccess(s -> context.completeNow())
+              .onFailure(context::failNow);
 
         context.awaitCompletion(5, TimeUnit.SECONDS);
     }
@@ -81,8 +83,7 @@
     void tearDown() throws InterruptedException
     {
         final CountDownLatch closeLatch = new CountDownLatch(1);
-        server.close(res -> closeLatch.countDown());
-        vertx.close();
+        server.close().onSuccess(res -> closeLatch.countDown());
         if (closeLatch.await(60, TimeUnit.SECONDS))
             logger.info("Close event received before timeout.");
         else
diff --git a/src/test/java/org/apache/cassandra/sidecar/routes/TimeSkewInfoHandlerTest.java b/src/test/java/org/apache/cassandra/sidecar/routes/TimeSkewInfoHandlerTest.java
index ddda2f0..ac2c264 100644
--- a/src/test/java/org/apache/cassandra/sidecar/routes/TimeSkewInfoHandlerTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/routes/TimeSkewInfoHandlerTest.java
@@ -33,14 +33,14 @@
 import com.google.inject.Module;
 import com.google.inject.util.Modules;
 import io.vertx.core.Vertx;
-import io.vertx.core.http.HttpServer;
 import io.vertx.ext.web.client.WebClient;
 import io.vertx.ext.web.codec.BodyCodec;
 import io.vertx.junit5.VertxExtension;
 import io.vertx.junit5.VertxTestContext;
-import org.apache.cassandra.sidecar.MainModule;
 import org.apache.cassandra.sidecar.TestModule;
 import org.apache.cassandra.sidecar.common.data.TimeSkewResponse;
+import org.apache.cassandra.sidecar.server.MainModule;
+import org.apache.cassandra.sidecar.server.Server;
 import org.apache.cassandra.sidecar.utils.TimeProvider;
 
 import static io.netty.handler.codec.http.HttpResponseStatus.OK;
@@ -55,7 +55,7 @@
     private static final Logger logger = LoggerFactory.getLogger(StreamSSTableComponentHandlerTest.class);
     private static final long TEST_TIMESTAMP = 12345L;
     private Vertx vertx;
-    private HttpServer server;
+    private Server server;
 
     @BeforeEach
     public void setUp() throws InterruptedException
@@ -64,9 +64,11 @@
         Injector injector = Guice.createInjector(Modules.override(new MainModule())
                                                         .with(new TestModule(), customTimeProvider));
         this.vertx = injector.getInstance(Vertx.class);
-        this.server = injector.getInstance(HttpServer.class);
+        this.server = injector.getInstance(Server.class);
         VertxTestContext context = new VertxTestContext();
-        server.listen(0, "127.0.0.1", context.succeedingThenComplete());
+        server.start()
+              .onSuccess(s -> context.completeNow())
+              .onFailure(context::failNow);
 
         context.awaitCompletion(5, TimeUnit.SECONDS);
     }
@@ -74,9 +76,8 @@
     @AfterEach
     void tearDown() throws InterruptedException
     {
-        final CountDownLatch closeLatch = new CountDownLatch(1);
-        server.close(res -> closeLatch.countDown());
-        vertx.close();
+        CountDownLatch closeLatch = new CountDownLatch(1);
+        server.close().onSuccess(res -> closeLatch.countDown());
         if (closeLatch.await(60, TimeUnit.SECONDS))
             logger.info("Close event received before timeout.");
         else
diff --git a/src/test/java/org/apache/cassandra/sidecar/routes/cassandra/NodeSettingsHandlerTest.java b/src/test/java/org/apache/cassandra/sidecar/routes/cassandra/NodeSettingsHandlerTest.java
index b06b027..2d9f0af 100644
--- a/src/test/java/org/apache/cassandra/sidecar/routes/cassandra/NodeSettingsHandlerTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/routes/cassandra/NodeSettingsHandlerTest.java
@@ -32,15 +32,15 @@
 import com.google.inject.util.Modules;
 import io.netty.handler.codec.http.HttpResponseStatus;
 import io.vertx.core.Vertx;
-import io.vertx.core.http.HttpServer;
 import io.vertx.core.json.JsonObject;
 import io.vertx.ext.web.client.WebClient;
 import io.vertx.ext.web.codec.BodyCodec;
 import io.vertx.junit5.VertxExtension;
 import io.vertx.junit5.VertxTestContext;
-import org.apache.cassandra.sidecar.MainModule;
 import org.apache.cassandra.sidecar.TestModule;
 import org.apache.cassandra.sidecar.common.NodeSettings;
+import org.apache.cassandra.sidecar.server.MainModule;
+import org.apache.cassandra.sidecar.server.Server;
 
 import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.NODE_SETTINGS_ROUTE;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -52,17 +52,19 @@
     private static final String URI_WITH_INSTANCE_ID = NODE_SETTINGS_ROUTE + "?instanceId=%s";
 
     private Vertx vertx;
-    private HttpServer server;
+    private Server server;
 
     @BeforeEach
     void setUp() throws InterruptedException
     {
         Injector injector = Guice.createInjector(Modules.override(new MainModule()).with(new TestModule()));
-        server = injector.getInstance(HttpServer.class);
+        server = injector.getInstance(Server.class);
         vertx = injector.getInstance(Vertx.class);
 
         VertxTestContext context = new VertxTestContext();
-        server.listen(0, "localhost", context.succeedingThenComplete());
+        server.start()
+              .onSuccess(s -> context.completeNow())
+              .onFailure(context::failNow);
 
         context.awaitCompletion(5, TimeUnit.SECONDS);
     }
@@ -70,9 +72,8 @@
     @AfterEach
     void tearDown() throws InterruptedException
     {
-        final CountDownLatch closeLatch = new CountDownLatch(1);
-        server.close(res -> closeLatch.countDown());
-        vertx.close();
+        CountDownLatch closeLatch = new CountDownLatch(1);
+        server.close().onSuccess(res -> closeLatch.countDown());
         if (closeLatch.await(60, TimeUnit.SECONDS))
             LOGGER.info("Close event received before timeout.");
         else
diff --git a/src/test/java/org/apache/cassandra/sidecar/routes/sstableuploads/BaseUploadsHandlerTest.java b/src/test/java/org/apache/cassandra/sidecar/routes/sstableuploads/BaseUploadsHandlerTest.java
index ed8ef81..1cff296 100644
--- a/src/test/java/org/apache/cassandra/sidecar/routes/sstableuploads/BaseUploadsHandlerTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/routes/sstableuploads/BaseUploadsHandlerTest.java
@@ -18,7 +18,6 @@
 
 package org.apache.cassandra.sidecar.routes.sstableuploads;
 
-import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -28,6 +27,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Stream;
 
+import com.google.common.util.concurrent.SidecarRateLimiter;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.io.TempDir;
@@ -40,23 +40,32 @@
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
+import com.google.inject.Key;
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
+import com.google.inject.name.Names;
 import com.google.inject.util.Modules;
 import io.vertx.core.Vertx;
-import io.vertx.core.http.HttpServer;
 import io.vertx.ext.web.client.WebClient;
 import io.vertx.junit5.VertxTestContext;
-import org.apache.cassandra.sidecar.MainModule;
 import org.apache.cassandra.sidecar.TestModule;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.cluster.InstancesConfig;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.config.SSTableUploadConfiguration;
+import org.apache.cassandra.sidecar.config.ServiceConfiguration;
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.config.TrafficShapingConfiguration;
 import org.apache.cassandra.sidecar.config.yaml.ServiceConfigurationImpl;
 import org.apache.cassandra.sidecar.config.yaml.SidecarConfigurationImpl;
+import org.apache.cassandra.sidecar.server.MainModule;
+import org.apache.cassandra.sidecar.server.Server;
 import org.apache.cassandra.sidecar.snapshots.SnapshotUtils;
 
+import static org.apache.cassandra.sidecar.config.yaml.TrafficShapingConfigurationImpl.DEFAULT_CHECK_INTERVAL;
+import static org.apache.cassandra.sidecar.config.yaml.TrafficShapingConfigurationImpl.DEFAULT_INBOUND_FILE_GLOBAL_BANDWIDTH_LIMIT;
+import static org.apache.cassandra.sidecar.config.yaml.TrafficShapingConfigurationImpl.DEFAULT_MAX_DELAY_TIME;
+import static org.apache.cassandra.sidecar.config.yaml.TrafficShapingConfigurationImpl.DEFAULT_OUTBOUND_GLOBAL_BANDWIDTH_LIMIT;
+import static org.apache.cassandra.sidecar.config.yaml.TrafficShapingConfigurationImpl.DEFAULT_PEAK_OUTBOUND_GLOBAL_BANDWIDTH_LIMIT;
 import static org.apache.cassandra.sidecar.snapshots.SnapshotUtils.mockInstancesConfig;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
@@ -70,34 +79,55 @@
 {
     protected final Logger logger = LoggerFactory.getLogger(this.getClass());
     protected Vertx vertx;
-    protected HttpServer server;
+    protected Server server;
     protected WebClient client;
     protected CassandraAdapterDelegate mockDelegate;
     protected SidecarConfiguration sidecarConfiguration;
     @TempDir
-    protected File temporaryFolder;
+    protected Path temporaryPath;
+    protected String canonicalTemporaryPath;
     protected SSTableUploadConfiguration mockSSTableUploadConfiguration;
+    protected TrafficShapingConfiguration trafficShapingConfiguration;
+    protected SidecarRateLimiter ingressFileRateLimiter;
 
     @BeforeEach
-    void setup() throws InterruptedException
+    void setup() throws InterruptedException, IOException
     {
+        canonicalTemporaryPath = temporaryPath.toFile().getCanonicalPath();
         mockDelegate = mock(CassandraAdapterDelegate.class);
         TestModule testModule = new TestModule();
         mockSSTableUploadConfiguration = mock(SSTableUploadConfiguration.class);
         when(mockSSTableUploadConfiguration.concurrentUploadsLimit()).thenReturn(3);
         when(mockSSTableUploadConfiguration.minimumSpacePercentageRequired()).thenReturn(0F);
-        sidecarConfiguration =
-        new SidecarConfigurationImpl(new ServiceConfigurationImpl(500, 1000L, mockSSTableUploadConfiguration));
+        trafficShapingConfiguration = mock(TrafficShapingConfiguration.class);
+        when(trafficShapingConfiguration.inboundGlobalBandwidthBytesPerSecond()).thenReturn(512 * 1024L);
+        when(trafficShapingConfiguration.outboundGlobalBandwidthBytesPerSecond())
+        .thenReturn(DEFAULT_OUTBOUND_GLOBAL_BANDWIDTH_LIMIT);
+        when(trafficShapingConfiguration.peakOutboundGlobalBandwidthBytesPerSecond())
+        .thenReturn(DEFAULT_PEAK_OUTBOUND_GLOBAL_BANDWIDTH_LIMIT);
+        when(trafficShapingConfiguration.maxDelayToWaitMillis()).thenReturn(DEFAULT_MAX_DELAY_TIME);
+        when(trafficShapingConfiguration.checkIntervalForStatsMillis()).thenReturn(DEFAULT_CHECK_INTERVAL);
+        when(trafficShapingConfiguration.inboundGlobalFileBandwidthBytesPerSecond())
+        .thenReturn(DEFAULT_INBOUND_FILE_GLOBAL_BANDWIDTH_LIMIT);
+        ServiceConfiguration serviceConfiguration =
+        ServiceConfigurationImpl.builder()
+                                .requestIdleTimeoutMillis(500)
+                                .requestTimeoutMillis(TimeUnit.SECONDS.toMillis(30))
+                                .ssTableUploadConfiguration(mockSSTableUploadConfiguration)
+                                .trafficShapingConfiguration(trafficShapingConfiguration)
+                                .build();
+        sidecarConfiguration = SidecarConfigurationImpl.builder()
+                                                       .serviceConfiguration(serviceConfiguration)
+                                                       .build();
         TestModuleOverride testModuleOverride = new TestModuleOverride(mockDelegate);
         Injector injector = Guice.createInjector(Modules.override(new MainModule())
                                                         .with(Modules.override(testModule)
                                                                      .with(testModuleOverride)));
-        server = injector.getInstance(HttpServer.class);
+        server = injector.getInstance(Server.class);
         vertx = injector.getInstance(Vertx.class);
         client = WebClient.create(vertx);
-
-        VertxTestContext context = new VertxTestContext();
-        server.listen(0, "localhost", context.succeedingThenComplete());
+        ingressFileRateLimiter = injector.getInstance(Key.get(SidecarRateLimiter.class,
+                                                              Names.named("IngressFileRateLimiter")));
 
         Metadata mockMetadata = mock(Metadata.class);
         KeyspaceMetadata mockKeyspaceMetadata = mock(KeyspaceMetadata.class);
@@ -106,6 +136,11 @@
         when(mockMetadata.getKeyspace("ks").getTable("tbl")).thenReturn(mockTableMetadata);
         when(mockDelegate.metadata()).thenReturn(mockMetadata);
 
+        VertxTestContext context = new VertxTestContext();
+        server.start()
+              .onSuccess(s -> context.completeNow())
+              .onFailure(context::failNow);
+
         context.awaitCompletion(5, TimeUnit.SECONDS);
     }
 
@@ -113,10 +148,9 @@
     void tearDown() throws InterruptedException
     {
         final CountDownLatch closeLatch = new CountDownLatch(1);
-        server.close(res -> closeLatch.countDown());
-        vertx.close();
+        server.close().onSuccess(res -> closeLatch.countDown());
         if (closeLatch.await(60, TimeUnit.SECONDS))
-            logger.info("Close event received before timeout.");
+            logger.debug("Close event received before timeout.");
         else
             logger.error("Close event timed out.");
     }
@@ -134,7 +168,7 @@
      */
     protected Path createStagedUploadFiles(UUID uploadId) throws IOException
     {
-        Path stagedUpload = Paths.get(SnapshotUtils.makeStagingDir(temporaryFolder.getCanonicalPath()))
+        Path stagedUpload = Paths.get(SnapshotUtils.makeStagingDir(canonicalTemporaryPath))
                                  .resolve(uploadId.toString())
                                  .resolve("ks")
                                  .resolve("table");
@@ -167,9 +201,9 @@
 
         @Provides
         @Singleton
-        public InstancesConfig instancesConfig() throws IOException
+        public InstancesConfig instancesConfig(Vertx vertx)
         {
-            return mockInstancesConfig(temporaryFolder.getCanonicalPath(), delegate, delegate, null, null);
+            return mockInstancesConfig(vertx, canonicalTemporaryPath, delegate, delegate, null, null);
         }
 
         @Singleton
diff --git a/src/test/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableUploadHandlerTest.java b/src/test/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableUploadHandlerTest.java
index 3338bca..26ca9dc 100644
--- a/src/test/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableUploadHandlerTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableUploadHandlerTest.java
@@ -19,6 +19,7 @@
 package org.apache.cassandra.sidecar.routes.sstableuploads;
 
 import java.io.IOException;
+import java.io.OutputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -26,6 +27,7 @@
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 
@@ -45,6 +47,7 @@
 import io.vertx.junit5.VertxTestContext;
 import org.apache.cassandra.sidecar.common.http.SidecarHttpResponseStatus;
 import org.apache.cassandra.sidecar.snapshots.SnapshotUtils;
+import org.assertj.core.data.Percentage;
 
 import static java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE;
 import static java.nio.file.attribute.PosixFilePermission.GROUP_READ;
@@ -62,7 +65,7 @@
  * Tests for the {@link SSTableUploadHandler}
  */
 @ExtendWith(VertxExtension.class)
-public class SSTableUploadHandlerTest extends BaseUploadsHandlerTest
+class SSTableUploadHandlerTest extends BaseUploadsHandlerTest
 {
     private static final String FILE_TO_BE_UPLOADED =
     "./src/test/resources/instance1/data/TestKeyspace/TestTable-54ea95ce-bba2-4e0a-a9be-e428e5d7160b/snapshots"
@@ -130,7 +133,7 @@
     }
 
     @Test
-    public void testInvalidUploadId(VertxTestContext context) throws IOException
+    void testInvalidUploadId(VertxTestContext context) throws IOException
     {
         sendUploadRequestAndVerify(null, context, "foo", "ks", "tbl", "with-lesser-content-length.db", "",
                                    Files.size(Paths.get(FILE_TO_BE_UPLOADED)), HttpResponseStatus.BAD_REQUEST.code(),
@@ -139,11 +142,11 @@
             assertThat(error.getString("status")).isEqualTo("Bad Request");
             assertThat(error.getInteger("code")).isEqualTo(400);
             assertThat(error.getString("message")).isEqualTo("Invalid upload id is supplied, uploadId=foo");
-        });
+        }, FILE_TO_BE_UPLOADED);
     }
 
     @Test
-    public void testInvalidKeyspace(VertxTestContext context) throws IOException
+    void testInvalidKeyspace(VertxTestContext context) throws IOException
     {
         UUID uploadId = UUID.randomUUID();
         sendUploadRequestAndVerify(context, uploadId, "invalidKeyspace", "tbl", "with-lesser-content-length.db", "",
@@ -152,7 +155,7 @@
     }
 
     @Test
-    public void testInvalidTable(VertxTestContext context) throws IOException
+    void testInvalidTable(VertxTestContext context) throws IOException
     {
         UUID uploadId = UUID.randomUUID();
         sendUploadRequestAndVerify(context, uploadId, "ks", "invalidTableName", "with-lesser-content-length.db", "",
@@ -161,7 +164,7 @@
     }
 
     @Test
-    public void testFreeSpacePercentCheckNotPassed(VertxTestContext context) throws IOException
+    void testFreeSpacePercentCheckNotPassed(VertxTestContext context) throws IOException
     {
         when(mockSSTableUploadConfiguration.minimumSpacePercentageRequired()).thenReturn(100F);
 
@@ -172,7 +175,7 @@
     }
 
     @Test
-    public void testConcurrentUploadLimitExceeded(VertxTestContext context) throws IOException
+    void testConcurrentUploadLimitExceeded(VertxTestContext context) throws IOException
     {
         when(mockSSTableUploadConfiguration.concurrentUploadsLimit()).thenReturn(0);
 
@@ -183,7 +186,7 @@
     }
 
     @Test
-    public void testPermitCleanup(VertxTestContext context) throws IOException, InterruptedException
+    void testPermitCleanup(VertxTestContext context) throws IOException, InterruptedException
     {
         when(mockSSTableUploadConfiguration.concurrentUploadsLimit()).thenReturn(1);
 
@@ -201,7 +204,7 @@
     }
 
     @Test
-    public void testFilePermissionOnUpload(VertxTestContext context) throws IOException
+    void testFilePermissionOnUpload(VertxTestContext context) throws IOException
     {
         String uploadId = UUID.randomUUID().toString();
         when(mockSSTableUploadConfiguration.filePermissions()).thenReturn("rwxr-xr-x");
@@ -210,12 +213,11 @@
                                    Files.size(Paths.get(FILE_TO_BE_UPLOADED)), HttpResponseStatus.OK.code(),
                                    false, response -> {
 
-            Path path = temporaryFolder.toPath()
-                                       .resolve("staging")
-                                       .resolve(uploadId)
-                                       .resolve("ks")
-                                       .resolve("tbl")
-                                       .resolve("without-md5.db");
+            Path path = temporaryPath.resolve("staging")
+                                     .resolve(uploadId)
+                                     .resolve("ks")
+                                     .resolve("tbl")
+                                     .resolve("without-md5.db");
 
             try
             {
@@ -233,7 +235,50 @@
             {
                 throw new RuntimeException(e);
             }
-        });
+        }, FILE_TO_BE_UPLOADED);
+    }
+
+    @Test
+    void testRateLimitedByIngressFileRateLimiterUpload(VertxTestContext context) throws IOException
+    {
+        // upper-bound configured to 512 KBps in BaseUploadsHandlerTest#setup
+        ingressFileRateLimiter.rate(256 * 1024L); // 256 KBps
+        Path largeFilePath = prepareTestFile(temporaryPath, "1MB-File-Data.db", 1024 * 1024); // 1MB
+
+        long startTime = System.nanoTime();
+        String uploadId = UUID.randomUUID().toString();
+        sendUploadRequestAndVerify(null, context, uploadId, "ks", "tbl", "1MB-File-Data.db", "",
+                                   Files.size(largeFilePath), HttpResponseStatus.OK.code(),
+                                   false, response -> {
+
+            // SSTable upload should take around 4 seconds (256 KB/s for a 1MB file)
+            long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
+            assertThat(response).isNotNull();
+            assertThat(elapsedMillis).isCloseTo(TimeUnit.SECONDS.toMillis(4),
+                                                Percentage.withPercentage(5));
+        }, largeFilePath.toString());
+    }
+
+    @Test
+    void testRateLimitedByGlobalLimiterUpload(VertxTestContext context) throws IOException
+    {
+        // upper-bound configured to 512 KBps in BaseUploadsHandlerTest#setup
+        // 1024 KBps Should not take effect, upper-bounded by global rate limiting
+        ingressFileRateLimiter.rate(1024 * 1024L);
+        Path largeFilePath = prepareTestFile(temporaryPath, "1MB-File-Data.db", 1024 * 1024); // 1MB
+
+        long startTime = System.nanoTime();
+        String uploadId = UUID.randomUUID().toString();
+        sendUploadRequestAndVerify(null, context, uploadId, "ks", "tbl", "1MB-File-Data.db", "",
+                                   Files.size(largeFilePath), HttpResponseStatus.OK.code(),
+                                   false, response -> {
+
+            // SSTable upload should take around 2 seconds (512 KB/s for a 1MB file)
+            long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
+            assertThat(response).isNotNull();
+            assertThat(elapsedMillis).isCloseTo(TimeUnit.SECONDS.toMillis(2),
+                                                Percentage.withPercentage(10));
+        }, largeFilePath.toString());
     }
 
     private void sendUploadRequestAndVerify(VertxTestContext context,
@@ -271,7 +316,8 @@
                                    fileLength,
                                    expectedRetCode,
                                    expectTimeout,
-                                   null);
+                                   null,
+                                   FILE_TO_BE_UPLOADED);
     }
 
     private void sendUploadRequestAndVerify(CountDownLatch latch,
@@ -284,7 +330,8 @@
                                             long fileLength,
                                             int expectedRetCode,
                                             boolean expectTimeout,
-                                            Consumer<HttpResponse<Buffer>> responseValidator)
+                                            Consumer<HttpResponse<Buffer>> responseValidator,
+                                            String fileToBeUploaded)
     {
         WebClient client = WebClient.create(vertx);
         String testRoute = "/api/v1/uploads/" + uploadId + "/keyspaces/" + keyspace
@@ -299,7 +346,7 @@
             req.putHeader(HttpHeaderNames.CONTENT_LENGTH.toString(), Long.toString(fileLength));
         }
 
-        AsyncFile fd = vertx.fileSystem().openBlocking(FILE_TO_BE_UPLOADED, new OpenOptions().setRead(true));
+        AsyncFile fd = vertx.fileSystem().openBlocking(fileToBeUploaded, new OpenOptions().setRead(true));
         req.sendStream(fd, response ->
         {
             if (expectTimeout)
@@ -320,7 +367,7 @@
 
             if (expectedRetCode == HttpResponseStatus.OK.code())
             {
-                Path targetFilePath = Paths.get(SnapshotUtils.makeStagingDir(temporaryFolder.getAbsolutePath()),
+                Path targetFilePath = Paths.get(SnapshotUtils.makeStagingDir(canonicalTemporaryPath),
                                                 uploadId, keyspace, tableName, targetFileName);
                 assertThat(Files.exists(targetFilePath)).isTrue();
             }
@@ -336,4 +383,24 @@
             client.close();
         });
     }
+
+    static Path prepareTestFile(Path directory, String fileName, long sizeInBytes) throws IOException
+    {
+        Path filePath = directory.resolve(fileName);
+        Files.deleteIfExists(filePath);
+
+        byte[] buffer = new byte[1024];
+        try (OutputStream outputStream = Files.newOutputStream(filePath))
+        {
+            int written = 0;
+            while (written < sizeInBytes)
+            {
+                ThreadLocalRandom.current().nextBytes(buffer);
+                int toWrite = (int) Math.min(buffer.length, sizeInBytes - written);
+                outputStream.write(buffer, 0, toWrite);
+                written += toWrite;
+            }
+        }
+        return filePath;
+    }
 }
diff --git a/src/test/java/org/apache/cassandra/sidecar/MainModuleTest.java b/src/test/java/org/apache/cassandra/sidecar/server/MainModuleTest.java
similarity index 94%
rename from src/test/java/org/apache/cassandra/sidecar/MainModuleTest.java
rename to src/test/java/org/apache/cassandra/sidecar/server/MainModuleTest.java
index cc0efd0..22b1198 100644
--- a/src/test/java/org/apache/cassandra/sidecar/MainModuleTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/server/MainModuleTest.java
@@ -16,13 +16,14 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.sidecar;
+package org.apache.cassandra.sidecar.server;
 
 import org.junit.jupiter.api.Test;
 
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 import com.google.inject.util.Modules;
+import org.apache.cassandra.sidecar.TestModule;
 import org.apache.cassandra.sidecar.common.utils.SidecarVersionProvider;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
diff --git a/src/test/java/org/apache/cassandra/sidecar/server/ServerSSLTest.java b/src/test/java/org/apache/cassandra/sidecar/server/ServerSSLTest.java
new file mode 100644
index 0000000..82cc746
--- /dev/null
+++ b/src/test/java/org/apache/cassandra/sidecar/server/ServerSSLTest.java
@@ -0,0 +1,475 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.server;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SSLHandshakeException;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.util.Modules;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import io.vertx.core.net.JksOptions;
+import io.vertx.core.net.PfxOptions;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.ext.web.client.WebClientOptions;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+import org.apache.cassandra.sidecar.TestModule;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.config.yaml.KeyStoreConfigurationImpl;
+import org.apache.cassandra.sidecar.config.yaml.ServiceConfigurationImpl;
+import org.apache.cassandra.sidecar.config.yaml.SidecarConfigurationImpl;
+import org.apache.cassandra.sidecar.config.yaml.SslConfigurationImpl;
+import org.assertj.core.api.InstanceOfAssertFactories;
+
+import static org.apache.cassandra.sidecar.common.ResourceUtils.writeResourceToPath;
+import static org.assertj.core.api.Assertions.as;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatRuntimeException;
+import static org.assertj.core.api.Assertions.from;
+
+/**
+ * Unit test for server with different SSL configurations
+ */
+@ExtendWith(VertxExtension.class)
+class ServerSSLTest
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(ServerSSLTest.class);
+    public static final String DEFAULT_PASSWORD = "password";
+
+    @TempDir
+    private Path certPath;
+    SidecarConfigurationImpl.Builder builder = SidecarConfigurationImpl.builder();
+    Injector injector;
+    Vertx vertx;
+    Server server;
+    Path serverKeyStoreP12Path;
+    Path clientKeyStoreP12Path;
+    Path expiredServerKeyStoreP12Path;
+    Path trustStoreP12Path;
+    Path serverKeyStoreJksPath;
+    Path trustStoreJksPath;
+    private KeyStoreConfigurationImpl p12TrustStore;
+    private KeyStoreConfigurationImpl p12KeyStore;
+
+    @BeforeEach
+    void setup()
+    {
+        ClassLoader classLoader = ServerSSLTest.class.getClassLoader();
+        serverKeyStoreP12Path = writeResourceToPath(classLoader, certPath, "certs/server_keystore.p12");
+        clientKeyStoreP12Path = writeResourceToPath(classLoader, certPath, "certs/client_keystore.p12");
+        expiredServerKeyStoreP12Path = writeResourceToPath(classLoader, certPath, "certs/expired_server_keystore.p12");
+        trustStoreP12Path = writeResourceToPath(classLoader, certPath, "certs/truststore.p12");
+        serverKeyStoreJksPath = writeResourceToPath(classLoader, certPath, "certs/server_keystore.jks");
+        trustStoreJksPath = writeResourceToPath(classLoader, certPath, "certs/truststore.jks");
+
+        p12TrustStore = new KeyStoreConfigurationImpl(trustStoreP12Path.toString(), DEFAULT_PASSWORD, "PKCS12", -1);
+        p12KeyStore = new KeyStoreConfigurationImpl(serverKeyStoreP12Path.toString(), DEFAULT_PASSWORD, "PKCS12", -1);
+
+        injector = Guice.createInjector(Modules.override(new MainModule())
+                                               .with(Modules.override(new TestModule())
+                                                            .with(new ServerSSLTestModule(builder))));
+        vertx = injector.getInstance(Vertx.class);
+    }
+
+    @AfterEach
+    void tearDown() throws InterruptedException
+    {
+        CountDownLatch closeLatch = new CountDownLatch(1);
+        server.close().onSuccess(res -> closeLatch.countDown());
+        if (closeLatch.await(60, TimeUnit.SECONDS))
+            LOGGER.info("Close event received before timeout.");
+        else
+            LOGGER.error("Close event timed out.");
+    }
+
+    @Test
+    void failsWhenKeyStoreIsNotConfigured()
+    {
+        builder.sslConfiguration(SslConfigurationImpl.builder().enabled(true).build());
+        server = server();
+
+        assertThatRuntimeException()
+        .isThrownBy(() -> server.start())
+        .withMessage("Invalid keystore parameters for SSL")
+        .withRootCauseInstanceOf(IllegalArgumentException.class)
+        .extracting(from(t -> t.getCause().getMessage()), as(InstanceOfAssertFactories.STRING))
+        .contains("keyStorePath and keyStorePassword must be set if ssl enabled");
+    }
+
+    @Test
+    void failsWhenKeyStorePasswordIsIncorrect()
+    {
+        SslConfigurationImpl ssl =
+        SslConfigurationImpl.builder()
+                            .enabled(true)
+                            .keystore(new KeyStoreConfigurationImpl(serverKeyStoreP12Path.toString(), "badpassword"))
+                            .build();
+        builder.sslConfiguration(ssl);
+        server = server();
+
+        assertThatRuntimeException()
+        .isThrownBy(() -> server.start())
+        .withMessage("Invalid keystore parameters for SSL")
+        .extracting(from(t -> t.getCause().getMessage()), as(InstanceOfAssertFactories.STRING))
+        .contains("keystore password was incorrect");
+    }
+
+    @Test
+    void failsWhenTrustStorePasswordIsIncorrect()
+    {
+        SslConfigurationImpl ssl =
+        SslConfigurationImpl.builder()
+                            .enabled(true)
+                            .keystore(new KeyStoreConfigurationImpl(serverKeyStoreP12Path.toString(), DEFAULT_PASSWORD))
+                            .truststore(new KeyStoreConfigurationImpl(trustStoreP12Path.toString(), "badpassword"))
+                            .build();
+        builder.sslConfiguration(ssl);
+        server = server();
+
+        assertThatRuntimeException()
+        .isThrownBy(() -> server.start())
+        .withMessage("Invalid keystore parameters for SSL")
+        .extracting(from(t -> t.getCause().getMessage()), as(InstanceOfAssertFactories.STRING))
+        .contains("keystore password was incorrect");
+    }
+
+    @Test
+    void testSSLWithPkcs12Succeeds(VertxTestContext context)
+    {
+        SslConfigurationImpl ssl =
+        SslConfigurationImpl.builder()
+                            .enabled(true)
+                            .keystore(p12KeyStore)
+                            .truststore(p12TrustStore)
+                            .build();
+
+        builder.sslConfiguration(ssl)
+               .serviceConfiguration(ServiceConfigurationImpl.builder()
+                                                             .host("127.0.0.1")
+                                                             .port(0)
+                                                             .build());
+        server = server();
+
+        server.start()
+              .compose(s -> validateHealthEndpoint(clientWithP12Keystore(true, false)))
+              .onComplete(context.succeedingThenComplete());
+    }
+
+    @Test
+    void testSSLWithJksSucceeds(VertxTestContext context)
+    {
+        SslConfigurationImpl ssl =
+        SslConfigurationImpl.builder()
+                            .enabled(true)
+                            .keystore(new KeyStoreConfigurationImpl(serverKeyStoreJksPath.toString(), DEFAULT_PASSWORD))
+                            .truststore(new KeyStoreConfigurationImpl(trustStoreJksPath.toString(), DEFAULT_PASSWORD))
+                            .build();
+
+        builder.sslConfiguration(ssl)
+               .serviceConfiguration(ServiceConfigurationImpl.builder()
+                                                             .host("127.0.0.1")
+                                                             .port(0)
+                                                             .build());
+        server = server();
+
+        server.start()
+              .compose(s -> validateHealthEndpoint(clientWithJksTrustStore()))
+              .onComplete(context.succeedingThenComplete());
+    }
+
+    @Test
+    void testOpenSSLSucceeds(VertxTestContext context)
+    {
+        SslConfigurationImpl ssl =
+        SslConfigurationImpl.builder()
+                            .enabled(true)
+                            .useOpenSsl(true)
+                            .keystore(p12KeyStore)
+                            .truststore(p12TrustStore)
+                            .build();
+
+        builder.sslConfiguration(ssl)
+               .serviceConfiguration(ServiceConfigurationImpl.builder()
+                                                             .host("127.0.0.1")
+                                                             .port(9043)
+                                                             .build());
+
+        server = server();
+
+        server.start()
+              .compose(s -> validateHealthEndpoint(clientWithP12Keystore(true, false)))
+              .onComplete(context.succeedingThenComplete());
+    }
+
+    @Test
+    void testTwoWaySSLSucceeds(VertxTestContext context)
+    {
+        SslConfigurationImpl ssl =
+        SslConfigurationImpl.builder()
+                            .enabled(true)
+                            .useOpenSsl(true)
+                            .keystore(p12KeyStore)
+                            .truststore(p12TrustStore)
+                            .clientAuth("REQUIRED")
+                            .build();
+
+        builder.sslConfiguration(ssl)
+               .serviceConfiguration(ServiceConfigurationImpl.builder()
+                                                             .host("127.0.0.1")
+                                                             .port(0)
+                                                             .build());
+        server = server();
+
+        server.start()
+              .compose(s -> validateHealthEndpoint(clientWithP12Keystore(true, true)))
+              .onComplete(context.succeedingThenComplete());
+    }
+
+    @Test
+    void failsOnMissingClientKeystore(VertxTestContext context)
+    {
+        SslConfigurationImpl ssl =
+        SslConfigurationImpl.builder()
+                            .enabled(true)
+                            .useOpenSsl(true)
+                            .keystore(p12KeyStore)
+                            .truststore(p12TrustStore)
+                            .clientAuth("REQUIRED")
+                            .build();
+
+        builder.sslConfiguration(ssl)
+               .serviceConfiguration(ServiceConfigurationImpl.builder()
+                                                             .host("127.0.0.1")
+                                                             .port(0)
+                                                             .build());
+        server = server();
+
+        server.start()
+              .compose(s -> validateHealthEndpoint(clientWithP12Keystore(true, false)))
+              .onComplete(context.failing(throwable -> {
+                  assertThat(throwable).isNotNull()
+                                       .hasMessageContaining("Received fatal alert: bad_certificate");
+                  context.completeNow();
+              }));
+    }
+
+    @Test
+    void testTwoWaySSLSucceedsWithOptionalClientAuth(VertxTestContext context)
+    {
+        SslConfigurationImpl ssl =
+        SslConfigurationImpl.builder()
+                            .enabled(true)
+                            .keystore(p12KeyStore)
+                            .truststore(p12TrustStore)
+                            .clientAuth("REQUEST") // client auth is optional
+                            .build();
+
+        builder.sslConfiguration(ssl)
+               .serviceConfiguration(ServiceConfigurationImpl.builder()
+                                                             .host("127.0.0.1")
+                                                             .port(0)
+                                                             .build());
+        server = server();
+
+        server.start()
+              .compose(s -> validateHealthEndpoint(clientWithP12Keystore(true, false)))
+              .onComplete(context.succeedingThenComplete());
+    }
+
+    @Test
+    void failsOnMissingClientTrustStore(VertxTestContext context)
+    {
+        SslConfigurationImpl ssl =
+        SslConfigurationImpl.builder()
+                            .enabled(true)
+                            .keystore(p12KeyStore)
+                            .truststore(p12TrustStore)
+                            .build();
+
+        builder.sslConfiguration(ssl)
+               .serviceConfiguration(ServiceConfigurationImpl.builder()
+                                                             .host("127.0.0.1")
+                                                             .port(0)
+                                                             .build());
+        server = server();
+
+        server.start()
+              .compose(s -> validateHealthEndpoint(clientWithP12Keystore(false, false)))
+              .onComplete(context.failing(throwable -> {
+                  assertThat(throwable).isNotNull()
+                                       .isInstanceOf(SSLHandshakeException.class)
+                                       .hasMessageContaining("Failed to create SSL connection");
+                  context.completeNow();
+              }));
+    }
+
+    @Test
+    void testHotReloadOfServerCertificates(VertxTestContext context)
+    {
+        KeyStoreConfigurationImpl expiredP12KeyStore =
+        new KeyStoreConfigurationImpl(expiredServerKeyStoreP12Path.toString(), DEFAULT_PASSWORD, "PKCS12", -1);
+        SslConfigurationImpl ssl =
+        SslConfigurationImpl.builder()
+                            .enabled(true)
+                            .keystore(expiredP12KeyStore)
+                            .truststore(p12TrustStore)
+                            .build();
+
+        int serverVerticleInstances = 16;
+        builder.sslConfiguration(ssl)
+               .serviceConfiguration(ServiceConfigurationImpl.builder()
+                                                             .host("127.0.0.1")
+                                                             // > 1 to ensure that hot reloading works for all
+                                                             // the deployed servers on each verticle instance
+                                                             .serverVerticleInstances(serverVerticleInstances)
+                                                             .port(9043)
+                                                             .build());
+
+        server = server();
+        WebClient client = clientWithP12Keystore(true, false);
+
+        server.start()
+              .compose(s -> {
+                  // Access the health endpoint, all the request are expected to fail with SSL connection errors
+                  // Try to hit all the deployed server verticles by iterating over the number of deployed instances
+                  List<Future<Void>> futureList = new ArrayList<>();
+                  for (int i = 0; i < serverVerticleInstances; i++)
+                  {
+                      int finalI = i;
+                      futureList.add(validateHealthEndpoint(client).onComplete(ar -> {
+                          assertThat(ar.cause()).as("The health endpoint request number " + finalI +
+                                                    " is expected to fail with the expired server cert")
+                                                .isNotNull()
+                                                .isInstanceOf(SSLHandshakeException.class)
+                                                .hasMessageContaining("Failed to create SSL connection");
+                      }));
+                  }
+                  return Future.all(futureList)
+                               .onSuccess(v -> context.failNow("Success is not expected when the keystore is expired"))
+                               .recover(t -> Future.succeededFuture());
+              })
+              .compose(v -> {
+                  try
+                  {
+                      // Override the expired certificate with a valid certificate
+                      Files.copy(serverKeyStoreP12Path, expiredServerKeyStoreP12Path,
+                                 StandardCopyOption.REPLACE_EXISTING);
+                  }
+                  catch (IOException e)
+                  {
+                      throw new RuntimeException(e);
+                  }
+                  // Force a reload of certificates in the server
+                  return server.updateSSLOptions(System.currentTimeMillis());
+              })
+              .compose(s -> {
+                  // Access the health endpoint, all the request are expected to succeed now that a valid
+                  // certificate has been loaded into the server. Try to hit all the deployed server verticles
+                  // by iterating over the number of deployed instances
+                  List<Future<Void>> futureList = new ArrayList<>();
+                  for (int i = 0; i < serverVerticleInstances; i++)
+                  {
+                      futureList.add(validateHealthEndpoint(client));
+                  }
+                  return Future.all(futureList);
+              })
+              .onComplete(context.succeedingThenComplete());
+    }
+
+    /**
+     * @return the server using the per-test SSL configuration
+     */
+    Server server()
+    {
+        return injector.getInstance(Server.class);
+    }
+
+    Future<Void> validateHealthEndpoint(WebClient client)
+    {
+        LOGGER.info("Checking server health localhost:{}/api/v1/__health", server.actualPort());
+        return client.get(server.actualPort(), "localhost", "/api/v1/__health")
+                     .send()
+                     .compose(response -> {
+                         assertThat(response.statusCode()).isEqualTo(HttpResponseStatus.OK.code());
+                         assertThat(response.bodyAsJsonObject().getString("status")).isEqualTo("OK");
+                         return Future.succeededFuture();
+                     });
+    }
+
+    WebClient clientWithP12Keystore(boolean includeTrustStore, boolean includeClientKeyStore)
+    {
+        WebClientOptions options = new WebClientOptions().setSsl(true);
+        if (includeTrustStore)
+        {
+            options.setTrustOptions(new PfxOptions().setPath(trustStoreP12Path.toString())
+                                                    .setPassword(DEFAULT_PASSWORD));
+        }
+        if (includeClientKeyStore)
+        {
+            options.setKeyCertOptions(new PfxOptions().setPath(clientKeyStoreP12Path.toString())
+                                                      .setPassword(DEFAULT_PASSWORD));
+        }
+        return WebClient.create(vertx, options);
+    }
+
+    WebClient clientWithJksTrustStore()
+    {
+        JksOptions trustOptions = new JksOptions().setPath(trustStoreJksPath.toString()).setPassword(DEFAULT_PASSWORD);
+        return WebClient.create(vertx, new WebClientOptions().setSsl(true).setTrustOptions(trustOptions));
+    }
+
+    static class ServerSSLTestModule extends AbstractModule
+    {
+        final SidecarConfigurationImpl.Builder builder;
+
+        ServerSSLTestModule(SidecarConfigurationImpl.Builder builder)
+        {
+            this.builder = builder;
+        }
+
+        @Provides
+        @Singleton
+        public SidecarConfiguration configuration()
+        {
+            return builder.build();
+        }
+    }
+}
diff --git a/src/test/java/org/apache/cassandra/sidecar/server/ServerTest.java b/src/test/java/org/apache/cassandra/sidecar/server/ServerTest.java
new file mode 100644
index 0000000..608d322
--- /dev/null
+++ b/src/test/java/org/apache/cassandra/sidecar/server/ServerTest.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.server;
+
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.junit5.Checkpoint;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+
+import static org.apache.cassandra.sidecar.common.ResourceUtils.writeResourceToPath;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatException;
+
+/**
+ * Unit tests for {@link Server} lifecycle
+ */
+@ExtendWith(VertxExtension.class)
+class ServerTest
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(ServerTest.class);
+    @TempDir
+    private Path confPath;
+
+    private Server server;
+    private Vertx vertx;
+    private WebClient client;
+
+    @BeforeEach
+    void setup()
+    {
+        ClassLoader classLoader = ServerTest.class.getClassLoader();
+        Path yamlPath = writeResourceToPath(classLoader, confPath, "config/sidecar_single_instance.yaml");
+        Injector injector = Guice.createInjector(new MainModule(yamlPath));
+        server = injector.getInstance(Server.class);
+        vertx = injector.getInstance(Vertx.class);
+        client = WebClient.create(vertx);
+    }
+
+    @Test
+    @DisplayName("Server should start and stop Sidecar successfully")
+    void startStopServer(VertxTestContext context)
+    {
+        Checkpoint serverStarted = context.checkpoint();
+        Checkpoint serverStopped = context.checkpoint();
+
+        vertx.eventBus().localConsumer(SidecarServerEvents.ON_SERVER_START.address(), message -> serverStarted.flag());
+        vertx.eventBus().localConsumer(SidecarServerEvents.ON_SERVER_STOP.address(), message -> serverStopped.flag());
+
+        server.start()
+              .compose(this::validateHealthEndpoint)
+              .compose(deploymentId -> server.stop(deploymentId))
+              .onFailure(context::failNow);
+    }
+
+    @Test
+    @DisplayName("Server should restart successfully")
+    void testServerRestarts(VertxTestContext context)
+    {
+        Checkpoint serverStarted = context.checkpoint(2);
+        Checkpoint serverStopped = context.checkpoint(2);
+
+        vertx.eventBus().localConsumer(SidecarServerEvents.ON_SERVER_START.address(), message -> serverStarted.flag());
+        vertx.eventBus().localConsumer(SidecarServerEvents.ON_SERVER_STOP.address(), message -> serverStopped.flag());
+
+        server.start()
+              .compose(this::validateHealthEndpoint)
+              .compose(deploymentId -> server.stop(deploymentId))
+              .compose(v -> server.start())
+              .compose(this::validateHealthEndpoint)
+              .compose(restartDeploymentId -> server.stop(restartDeploymentId))
+              .onFailure(context::failNow);
+    }
+
+    @Test
+    @DisplayName("Server should start and close Sidecar successfully")
+    void startCloseServer(VertxTestContext context)
+    {
+        Checkpoint serverStarted = context.checkpoint();
+        Checkpoint serverStopped = context.checkpoint();
+
+        vertx.eventBus().localConsumer(SidecarServerEvents.ON_SERVER_START.address(), message -> serverStarted.flag());
+        vertx.eventBus().localConsumer(SidecarServerEvents.ON_SERVER_STOP.address(), message -> serverStopped.flag());
+
+        server.start()
+              .compose(this::validateHealthEndpoint)
+              .compose(deploymentId -> server.close())
+              .onFailure(context::failNow);
+    }
+
+    @Test
+    @DisplayName("Server should start and close Sidecar successfully and start is no longer allowed")
+    void startCloseServerShouldNotStartAgain(VertxTestContext context)
+    {
+        Checkpoint serverStarted = context.checkpoint();
+        Checkpoint serverStopped = context.checkpoint();
+
+        vertx.eventBus().localConsumer(SidecarServerEvents.ON_SERVER_START.address(), message -> serverStarted.flag());
+        vertx.eventBus().localConsumer(SidecarServerEvents.ON_SERVER_STOP.address(), message -> serverStopped.flag());
+
+        server.start()
+              .compose(this::validateHealthEndpoint)
+              .compose(deploymentId -> server.close())
+              .onSuccess(v -> assertThatException().isThrownBy(() -> server.start())
+                                                   .withMessageContaining("Vert.x closed"))
+              .onFailure(context::failNow);
+    }
+
+    Future<String> validateHealthEndpoint(String deploymentId)
+    {
+        LOGGER.info("Checking server health 127.0.0.1:{}/api/v1/__health", server.actualPort());
+        return client.get(server.actualPort(), "127.0.0.1", "/api/v1/__health")
+                     .send()
+                     .compose(response -> {
+                         assertThat(response.statusCode()).isEqualTo(HttpResponseStatus.OK.code());
+                         assertThat(response.bodyAsJsonObject().getString("status")).isEqualTo("OK");
+                         return Future.succeededFuture(deploymentId);
+                     });
+    }
+}
diff --git a/src/test/java/org/apache/cassandra/sidecar/snapshots/SnapshotSearchTest.java b/src/test/java/org/apache/cassandra/sidecar/snapshots/SnapshotSearchTest.java
index a020fb5..b3b1797 100644
--- a/src/test/java/org/apache/cassandra/sidecar/snapshots/SnapshotSearchTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/snapshots/SnapshotSearchTest.java
@@ -18,8 +18,8 @@
 
 package org.apache.cassandra.sidecar.snapshots;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -53,7 +53,7 @@
 public class SnapshotSearchTest
 {
     @TempDir
-    File temporaryFolder;
+    Path temporaryFolder;
 
     SnapshotPathBuilder instance;
     Vertx vertx = Vertx.vertx();
@@ -62,9 +62,9 @@
     @BeforeEach
     public void setup() throws IOException
     {
-        rootDir = temporaryFolder.getCanonicalPath();
-        SnapshotUtils.initializeTmpDirectory(temporaryFolder);
-        InstancesConfig mockInstancesConfig = mockInstancesConfig(rootDir);
+        rootDir = temporaryFolder.toFile().getCanonicalPath();
+        SnapshotUtils.initializeTmpDirectory(temporaryFolder.toFile());
+        InstancesConfig mockInstancesConfig = mockInstancesConfig(vertx, rootDir);
 
         CassandraInputValidator validator = new CassandraInputValidator();
         ExecutorPools executorPools = new ExecutorPools(vertx, new ServiceConfigurationImpl());
@@ -126,15 +126,13 @@
         Collections.sort(snapshotDirectories);
         assertThat(snapshotDirectories).isEqualTo(expectedDirectories);
 
-        //noinspection rawtypes
-        List<Future> futures = snapshotDirectories.stream()
-                                                  .map(directory -> instance
-                                                                    .listSnapshotDirectory(directory,
-                                                                                           includeSecondaryIndexFiles))
-                                                  .collect(Collectors.toList());
+        List<Future<List<SnapshotPathBuilder.SnapshotFile>>> futures =
+        snapshotDirectories.stream()
+                           .map(directory -> instance.listSnapshotDirectory(directory, includeSecondaryIndexFiles))
+                           .collect(Collectors.toList());
 
         VertxTestContext compositeFutureContext = new VertxTestContext();
-        CompositeFuture ar = CompositeFuture.all(futures);
+        CompositeFuture ar = Future.all(futures);
         ar.onComplete(compositeFutureContext.succeedingThenComplete());
         assertThat(compositeFutureContext.awaitCompletion(5, TimeUnit.SECONDS)).isTrue();
         assertThat(compositeFutureContext.failed()).isFalse();
diff --git a/src/test/java/org/apache/cassandra/sidecar/snapshots/SnapshotUtils.java b/src/test/java/org/apache/cassandra/sidecar/snapshots/SnapshotUtils.java
index c0f7af7..b6a0c92 100644
--- a/src/test/java/org/apache/cassandra/sidecar/snapshots/SnapshotUtils.java
+++ b/src/test/java/org/apache/cassandra/sidecar/snapshots/SnapshotUtils.java
@@ -28,15 +28,16 @@
 import java.util.Collections;
 import java.util.List;
 
+import io.vertx.core.Vertx;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.cluster.InstancesConfig;
 import org.apache.cassandra.sidecar.cluster.InstancesConfigImpl;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadataImpl;
 import org.apache.cassandra.sidecar.common.CQLSessionProvider;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
-import org.apache.cassandra.sidecar.common.CassandraVersionProvider;
 import org.apache.cassandra.sidecar.common.MockCassandraFactory;
 import org.apache.cassandra.sidecar.common.dns.DnsResolver;
+import org.apache.cassandra.sidecar.utils.CassandraVersionProvider;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
@@ -90,14 +91,15 @@
                    .setLastModified(System.currentTimeMillis() + 2_000_000)).isTrue();
     }
 
-    public static InstancesConfig mockInstancesConfig(String rootPath)
+    public static InstancesConfig mockInstancesConfig(Vertx vertx, String rootPath)
     {
         CQLSessionProvider mockSession1 = mock(CQLSessionProvider.class);
         CQLSessionProvider mockSession2 = mock(CQLSessionProvider.class);
-        return mockInstancesConfig(rootPath, null, null, mockSession1, mockSession2);
+        return mockInstancesConfig(vertx, rootPath, null, null, mockSession1, mockSession2);
     }
 
-    public static InstancesConfig mockInstancesConfig(String rootPath,
+    public static InstancesConfig mockInstancesConfig(Vertx vertx,
+                                                      String rootPath,
                                                       CassandraAdapterDelegate delegate1,
                                                       CassandraAdapterDelegate delegate2,
                                                       CQLSessionProvider cqlSessionProvider1,
@@ -109,12 +111,12 @@
 
         if (delegate1 == null)
         {
-            delegate1 = new CassandraAdapterDelegate(versionProvider, cqlSessionProvider1, null, null);
+            delegate1 = new CassandraAdapterDelegate(vertx, 1, versionProvider, cqlSessionProvider1, null, null);
         }
 
         if (delegate2 == null)
         {
-            delegate2 = new CassandraAdapterDelegate(versionProvider, cqlSessionProvider2, null, null);
+            delegate2 = new CassandraAdapterDelegate(vertx, 2, versionProvider, cqlSessionProvider2, null, null);
         }
 
         InstanceMetadataImpl localhost = InstanceMetadataImpl.builder()
diff --git a/src/test/java/org/apache/cassandra/sidecar/tasks/HealthCheckPeriodicTaskTest.java b/src/test/java/org/apache/cassandra/sidecar/tasks/HealthCheckPeriodicTaskTest.java
new file mode 100644
index 0000000..ea3a770
--- /dev/null
+++ b/src/test/java/org/apache/cassandra/sidecar/tasks/HealthCheckPeriodicTaskTest.java
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.tasks;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import io.vertx.core.Promise;
+import io.vertx.core.Vertx;
+import io.vertx.junit5.Checkpoint;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.InstancesConfig;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.config.HealthCheckConfiguration;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.config.yaml.ServiceConfigurationImpl;
+import org.mockito.stubbing.Answer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for the {@link HealthCheckPeriodicTask}
+ */
+@ExtendWith(VertxExtension.class)
+class HealthCheckPeriodicTaskTest
+{
+    SidecarConfiguration mockConfiguration;
+    HealthCheckConfiguration mockHealthCheckConfiguration;
+    HealthCheckPeriodicTask healthCheck;
+    InstancesConfig mockInstancesConfig;
+
+    @BeforeEach
+    void setup()
+    {
+        mockConfiguration = mock(SidecarConfiguration.class);
+        mockHealthCheckConfiguration = mock(HealthCheckConfiguration.class);
+        when(mockConfiguration.healthCheckConfiguration()).thenReturn(mockHealthCheckConfiguration);
+        when(mockHealthCheckConfiguration.initialDelayMillis()).thenReturn(10);
+        when(mockHealthCheckConfiguration.checkIntervalMillis()).thenReturn(1000);
+
+        mockInstancesConfig = mock(InstancesConfig.class);
+
+        Vertx vertx = Vertx.vertx();
+        ExecutorPools executorPools = new ExecutorPools(vertx, new ServiceConfigurationImpl());
+        healthCheck = new HealthCheckPeriodicTask(vertx, mockConfiguration, mockInstancesConfig, executorPools);
+    }
+
+    @Test
+    void testConfiguration()
+    {
+        assertThat(healthCheck.initialDelay()).isEqualTo(10);
+        assertThat(healthCheck.delay()).isEqualTo(1000);
+        assertThat(healthCheck.name()).isEqualTo("Health Check");
+    }
+
+    @Test
+    void testHealthCheckInvokedForAllInstances(VertxTestContext context)
+    {
+        int numberOfInstances = 5;
+        Checkpoint healthCheckCheckPoint = context.checkpoint(numberOfInstances);
+        List<InstanceMetadata> mockInstanceMetadata =
+        buildMockInstanceMetadata(healthCheckCheckPoint, numberOfInstances);
+        when(mockInstancesConfig.instances()).thenReturn(mockInstanceMetadata);
+        Promise<Void> promise = Promise.promise();
+        healthCheck.execute(promise);
+        promise.future().onComplete(context.succeedingThenComplete());
+    }
+
+    @Test
+    void testInstanceMetadataExceptionDoesntPreventChecksOnOtherInstances(VertxTestContext context)
+    {
+        int numberOfInstances = 5;
+        Checkpoint healthCheckCheckPoint = context.checkpoint(numberOfInstances);
+        List<InstanceMetadata> mockInstanceMetadata =
+        buildMockInstanceMetadata(healthCheckCheckPoint, numberOfInstances);
+        InstanceMetadata mockInstance = mock(InstanceMetadata.class);
+        when(mockInstance.delegate()).thenThrow(new RuntimeException());
+        mockInstanceMetadata.add(3, mockInstance);
+        when(mockInstancesConfig.instances()).thenReturn(mockInstanceMetadata);
+        Promise<Void> promise = Promise.promise();
+        healthCheck.execute(promise);
+        promise.future().onComplete(context.succeedingThenComplete());
+    }
+
+    @Test
+    void testDelegateExceptionDoesntPreventChecksOnOtherInstances(VertxTestContext context)
+    {
+        int numberOfInstances = 5;
+        Checkpoint healthCheckCheckPoint = context.checkpoint(numberOfInstances);
+        List<InstanceMetadata> mockInstanceMetadata =
+        buildMockInstanceMetadata(healthCheckCheckPoint, numberOfInstances);
+        InstanceMetadata mockInstance = mock(InstanceMetadata.class);
+        CassandraAdapterDelegate mockDelegate = mock(CassandraAdapterDelegate.class);
+        when(mockInstance.delegate()).thenReturn(mockDelegate);
+        doThrow(new RuntimeException()).when(mockDelegate).healthCheck();
+        mockInstanceMetadata.add(3, mockInstance);
+        when(mockInstancesConfig.instances()).thenReturn(mockInstanceMetadata);
+        Promise<Void> promise = Promise.promise();
+        healthCheck.execute(promise);
+        promise.future().onComplete(context.succeedingThenComplete());
+    }
+
+    private List<InstanceMetadata> buildMockInstanceMetadata(Checkpoint healthCheckCheckPoint, int numberOfInstances)
+    {
+        return IntStream.range(0, numberOfInstances)
+                        .mapToObj(i -> {
+                            InstanceMetadata mockInstanceMetadata = mock(InstanceMetadata.class);
+                            CassandraAdapterDelegate mockDelegate = mock(CassandraAdapterDelegate.class);
+
+                            doAnswer((Answer<Void>) invocation -> {
+                                healthCheckCheckPoint.flag();
+                                return null;
+                            }).when(mockDelegate).healthCheck();
+                            when(mockInstanceMetadata.delegate()).thenReturn(mockDelegate);
+                            return mockInstanceMetadata;
+                        })
+                        .collect(Collectors.toList());
+    }
+}
diff --git a/src/test/java/org/apache/cassandra/sidecar/tasks/PeriodicTaskExecutorTest.java b/src/test/java/org/apache/cassandra/sidecar/tasks/PeriodicTaskExecutorTest.java
new file mode 100644
index 0000000..a11f7f1
--- /dev/null
+++ b/src/test/java/org/apache/cassandra/sidecar/tasks/PeriodicTaskExecutorTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.tasks;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.util.concurrent.Uninterruptibles;
+import org.junit.jupiter.api.Test;
+
+import io.vertx.core.Promise;
+import io.vertx.core.Vertx;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.config.yaml.ServiceConfigurationImpl;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for the {@link PeriodicTaskExecutor} class
+ */
+class PeriodicTaskExecutorTest
+{
+    @Test
+    void testLoopFailure()
+    {
+        Vertx vertx = Vertx.vertx();
+        ExecutorPools executorPools = new ExecutorPools(vertx, new ServiceConfigurationImpl());
+        PeriodicTaskExecutor taskExecutor = new PeriodicTaskExecutor(executorPools);
+
+        int totalFailures = 5;
+        AtomicInteger failuresCount = new AtomicInteger(0);
+        CountDownLatch closeLatch = new CountDownLatch(1);
+        AtomicBoolean isClosed = new AtomicBoolean(false);
+        taskExecutor.schedule(new PeriodicTask()
+        {
+            @Override
+            public long delay()
+            {
+                return 20;
+            }
+
+            @Override
+            public void execute(Promise<Void> promise)
+            {
+                if (failuresCount.incrementAndGet() == totalFailures)
+                {
+                    taskExecutor.unschedule(this);
+                }
+                throw new RuntimeException("ah, it failed");
+            }
+
+            @Override
+            public void close()
+            {
+                isClosed.set(true);
+                closeLatch.countDown();
+            }
+        });
+        Uninterruptibles.awaitUninterruptibly(closeLatch);
+        assertThat(isClosed.get()).isTrue();
+        assertThat(failuresCount.get()).isEqualTo(totalFailures);
+    }
+}
diff --git a/src/test/java/org/apache/cassandra/sidecar/utils/CacheFactoryTest.java b/src/test/java/org/apache/cassandra/sidecar/utils/CacheFactoryTest.java
index f4ecb6c..f8b9e35 100644
--- a/src/test/java/org/apache/cassandra/sidecar/utils/CacheFactoryTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/utils/CacheFactoryTest.java
@@ -62,7 +62,10 @@
 
         SSTableImportConfiguration ssTableImportConfiguration =
         new SSTableImportConfigurationImpl(ssTableImportCacheConfiguration);
-        ServiceConfiguration serviceConfiguration = new ServiceConfigurationImpl(ssTableImportConfiguration);
+        ServiceConfiguration serviceConfiguration =
+        ServiceConfigurationImpl.builder()
+                                .ssTableImportConfiguration(ssTableImportConfiguration)
+                                .build();
         SSTableImporter mockSSTableImporter = mock(SSTableImporter.class);
         cacheFactory = new CacheFactory(serviceConfiguration, mockSSTableImporter, fakeTicker::read);
     }
diff --git a/src/test/java/org/apache/cassandra/sidecar/utils/SSTableImporterTest.java b/src/test/java/org/apache/cassandra/sidecar/utils/SSTableImporterTest.java
index ce690ba..957630c 100644
--- a/src/test/java/org/apache/cassandra/sidecar/utils/SSTableImporterTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/utils/SSTableImporterTest.java
@@ -30,7 +30,7 @@
 import io.vertx.ext.web.handler.HttpException;
 import io.vertx.junit5.VertxExtension;
 import io.vertx.junit5.VertxTestContext;
-import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.common.TableOperations;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
 import org.apache.cassandra.sidecar.config.ServiceConfiguration;
@@ -59,7 +59,10 @@
     public void setup() throws InterruptedException
     {
         vertx = Vertx.vertx();
-        serviceConfiguration = new ServiceConfigurationImpl(new SSTableImportConfigurationImpl(10));
+        serviceConfiguration =
+        ServiceConfigurationImpl.builder()
+                                .ssTableImportConfiguration(new SSTableImportConfigurationImpl(10))
+                                .build();
 
         mockMetadataFetcher = mock(InstanceMetadataFetcher.class);
         CassandraAdapterDelegate mockCassandraAdapterDelegate1 = mock(CassandraAdapterDelegate.class);
@@ -201,7 +204,10 @@
     @Test
     void testCancelImportSucceeds(VertxTestContext context)
     {
-        serviceConfiguration = new ServiceConfigurationImpl(new SSTableImportConfigurationImpl(500));
+        serviceConfiguration =
+        ServiceConfigurationImpl.builder()
+                                .ssTableImportConfiguration(new SSTableImportConfigurationImpl(500))
+                                .build();
 
         SSTableImporter importer = new SSTableImporter(vertx, mockMetadataFetcher, serviceConfiguration, executorPools,
                                                        mockUploadPathBuilder);
diff --git a/common/src/test/java/org/apache/cassandra/sidecar/common/SimpleCassandraVersionProviderTest.java b/src/test/java/org/apache/cassandra/sidecar/utils/SimpleCassandraVersionProviderTest.java
similarity index 95%
rename from common/src/test/java/org/apache/cassandra/sidecar/common/SimpleCassandraVersionProviderTest.java
rename to src/test/java/org/apache/cassandra/sidecar/utils/SimpleCassandraVersionProviderTest.java
index 9054e1d..67fcc44 100644
--- a/common/src/test/java/org/apache/cassandra/sidecar/common/SimpleCassandraVersionProviderTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/utils/SimpleCassandraVersionProviderTest.java
@@ -16,11 +16,12 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.sidecar.common;
+package org.apache.cassandra.sidecar.utils;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import org.apache.cassandra.sidecar.common.ICassandraFactory;
 import org.apache.cassandra.sidecar.mocks.V30;
 import org.apache.cassandra.sidecar.mocks.V40;
 import org.apache.cassandra.sidecar.mocks.V41;
diff --git a/common/src/test/java/org/apache/cassandra/sidecar/common/SimpleCassandraVersionTest.java b/src/test/java/org/apache/cassandra/sidecar/utils/SimpleCassandraVersionTest.java
similarity index 98%
rename from common/src/test/java/org/apache/cassandra/sidecar/common/SimpleCassandraVersionTest.java
rename to src/test/java/org/apache/cassandra/sidecar/utils/SimpleCassandraVersionTest.java
index a75ff8e..7583691 100644
--- a/common/src/test/java/org/apache/cassandra/sidecar/common/SimpleCassandraVersionTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/utils/SimpleCassandraVersionTest.java
@@ -16,9 +16,10 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.sidecar.common;
+package org.apache.cassandra.sidecar.utils;
 
 import org.junit.jupiter.api.Test;
+
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
 
diff --git a/src/test/resources/certs/client_keystore.jks b/src/test/resources/certs/client_keystore.jks
new file mode 100644
index 0000000..f08e983
--- /dev/null
+++ b/src/test/resources/certs/client_keystore.jks
Binary files differ
diff --git a/src/test/resources/certs/client_keystore.p12 b/src/test/resources/certs/client_keystore.p12
new file mode 100644
index 0000000..7c504af
--- /dev/null
+++ b/src/test/resources/certs/client_keystore.p12
Binary files differ
diff --git a/src/test/resources/certs/expired_server_keystore.jks b/src/test/resources/certs/expired_server_keystore.jks
new file mode 100644
index 0000000..e9bbda1
--- /dev/null
+++ b/src/test/resources/certs/expired_server_keystore.jks
Binary files differ
diff --git a/src/test/resources/certs/expired_server_keystore.p12 b/src/test/resources/certs/expired_server_keystore.p12
new file mode 100644
index 0000000..acfae0a
--- /dev/null
+++ b/src/test/resources/certs/expired_server_keystore.p12
Binary files differ
diff --git a/src/test/resources/certs/generate-certs.sh b/src/test/resources/certs/generate-certs.sh
new file mode 100755
index 0000000..65de8af
--- /dev/null
+++ b/src/test/resources/certs/generate-certs.sh
@@ -0,0 +1,56 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# A utility script to re-generate the certificates used for testing
+# the password is password
+
+set -xe
+SCRIPT_DIR=$( dirname -- "$( readlink -f -- "$0"; )"; )
+
+# clean up existing truststore files
+rm -f "${SCRIPT_DIR}"/truststore.p12 "${SCRIPT_DIR}"/truststore.jks "${SCRIPT_DIR}"/server_keystore.p12 \
+  "${SCRIPT_DIR}"/server_keystore.jks "${SCRIPT_DIR}"/client_keystore.p12 "${SCRIPT_DIR}"/client_keystore.jks \
+  "${SCRIPT_DIR}"/expired_server_keystore.p12
+
+# Create root CA key and PEM file
+openssl req -new -x509 -days 1825 -nodes -newkey rsa:2048 -sha512 \
+  -out "${SCRIPT_DIR}"/rootCA.pem -keyout "${SCRIPT_DIR}"/rootCA.key \
+  -subj "/CN=cs.local/O=Apache Cassandra/L=San Jose/ST=California/C=US"
+
+# Generate PKCS12 truststore with root CA
+keytool -keystore "${SCRIPT_DIR}"/truststore.p12 \
+  -alias fakerootca \
+  -importcert -noprompt \
+  -file "${SCRIPT_DIR}"/rootCA.pem \
+  -keypass "password" \
+  -storepass "password"
+
+# Generate JKS truststore
+keytool -importkeystore -srckeystore "${SCRIPT_DIR}"/truststore.p12 -srcstoretype pkcs12 \
+  -srcalias fakerootca -destkeystore "${SCRIPT_DIR}"/truststore.jks -deststoretype jks \
+  -deststorepass "password" -destalias fakerootca -srcstorepass "password"
+
+# Generate the server keystore
+"${SCRIPT_DIR}"/generate-server-keystore.sh
+
+# Generate the client keystore
+"${SCRIPT_DIR}"/generate-client-keystore.sh
+
+# Generate the expired server keystore
+CERT_VALIDITY_DAYS=-1 FILE_NAME=expired_server_keystore "${SCRIPT_DIR}"/generate-server-keystore.sh
diff --git a/src/test/resources/certs/generate-client-keystore.sh b/src/test/resources/certs/generate-client-keystore.sh
new file mode 100755
index 0000000..b6dcd48
--- /dev/null
+++ b/src/test/resources/certs/generate-client-keystore.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# A utility script to re-generate the server keystores used for testing
+
+CERT_VALIDITY_DAYS=${CERT_VALIDITY_DAYS:-1825}
+FILE_NAME=${FILE_NAME:-client_keystore}
+
+# Generate client key
+openssl req -new -multivalue-rdn -keyout "${SCRIPT_DIR}"/"${FILE_NAME}".key -nodes -newkey rsa:2048 \
+  -subj "/UID=identity:sidecar-client-identity/CN=cs.local/O=Apache Cassandra/L=San Jose/ST=California/C=US" |
+  openssl x509 -req -CAkey "${SCRIPT_DIR}"/rootCA.key -CA "${SCRIPT_DIR}"/rootCA.pem -days "${CERT_VALIDITY_DAYS}" \
+    -set_serial $RANDOM -sha512 -out "${SCRIPT_DIR}"/"${FILE_NAME}".pem
+
+# Chain client certs
+cat "${SCRIPT_DIR}"/"${FILE_NAME}".pem "${SCRIPT_DIR}"/rootCA.pem >"${SCRIPT_DIR}"/"${FILE_NAME}"_chain.pem
+
+# Generate the client keystore
+openssl pkcs12 -export -out "${SCRIPT_DIR}"/"${FILE_NAME}".p12 -in "${SCRIPT_DIR}"/"${FILE_NAME}"_chain.pem \
+  -inkey "${SCRIPT_DIR}"/"${FILE_NAME}".key -name "${FILE_NAME}" \
+  -passin pass:"password" -passout pass:"password"
+
+# Generate JKS client keystore
+keytool -importkeystore -srckeystore "${SCRIPT_DIR}"/"${FILE_NAME}".p12 -srcstoretype pkcs12 \
+  -srcalias "${FILE_NAME}" -destkeystore "${SCRIPT_DIR}"/"${FILE_NAME}".jks -deststoretype jks \
+  -deststorepass "password" -destalias "${FILE_NAME}" -srcstorepass "password"
diff --git a/src/test/resources/certs/generate-server-keystore.sh b/src/test/resources/certs/generate-server-keystore.sh
new file mode 100755
index 0000000..c052083
--- /dev/null
+++ b/src/test/resources/certs/generate-server-keystore.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# A utility script to re-generate the server keystores used for testing
+
+CERT_VALIDITY_DAYS=${CERT_VALIDITY_DAYS:-1825}
+FILE_NAME=${FILE_NAME:-server_keystore}
+
+# Generate server key
+openssl req -new -multivalue-rdn -keyout "${SCRIPT_DIR}"/"${FILE_NAME}".key -nodes -newkey rsa:2048 \
+  -subj "/CN=cs.local/O=Apache Cassandra/L=San Jose/ST=California/C=US" \
+  -out "${SCRIPT_DIR}"/"${FILE_NAME}".csr
+
+# Sign the server key with the root CA key
+openssl x509 -req -CAkey "${SCRIPT_DIR}"/rootCA.key -CA "${SCRIPT_DIR}"/rootCA.pem \
+  -days "${CERT_VALIDITY_DAYS}" -set_serial $RANDOM -sha512 -out "${SCRIPT_DIR}"/"${FILE_NAME}".pem -in "${SCRIPT_DIR}"/"${FILE_NAME}".csr \
+  -extfile <(printf "subjectAltName=DNS:localhost,DNS:127.0.0.1,DNS:::1,DNS:cs.local")
+
+# Chain server certs
+cat "${SCRIPT_DIR}"/"${FILE_NAME}".pem "${SCRIPT_DIR}"/rootCA.pem >"${SCRIPT_DIR}"/"${FILE_NAME}"_chain.pem
+
+# Generate the server keystore
+openssl pkcs12 -export -out "${SCRIPT_DIR}"/"${FILE_NAME}".p12 -in "${SCRIPT_DIR}"/"${FILE_NAME}"_chain.pem \
+  -inkey "${SCRIPT_DIR}"/"${FILE_NAME}".key -name "${FILE_NAME}" \
+  -passin pass:"password" -passout pass:"password"
+
+# Generate JKS server keystore
+keytool -importkeystore -srckeystore "${SCRIPT_DIR}"/"${FILE_NAME}".p12 -srcstoretype pkcs12 \
+  -srcalias "${FILE_NAME}" -destkeystore "${SCRIPT_DIR}"/"${FILE_NAME}".jks -deststoretype jks \
+  -deststorepass "password" -destalias "${FILE_NAME}" -srcstorepass "password"
diff --git a/src/test/resources/certs/server_keystore.jks b/src/test/resources/certs/server_keystore.jks
new file mode 100644
index 0000000..5b369dc
--- /dev/null
+++ b/src/test/resources/certs/server_keystore.jks
Binary files differ
diff --git a/src/test/resources/certs/server_keystore.p12 b/src/test/resources/certs/server_keystore.p12
new file mode 100644
index 0000000..07f58fa
--- /dev/null
+++ b/src/test/resources/certs/server_keystore.p12
Binary files differ
diff --git a/src/test/resources/certs/truststore.jks b/src/test/resources/certs/truststore.jks
new file mode 100644
index 0000000..40364fa
--- /dev/null
+++ b/src/test/resources/certs/truststore.jks
Binary files differ
diff --git a/src/test/resources/certs/truststore.p12 b/src/test/resources/certs/truststore.p12
new file mode 100644
index 0000000..820ce34
--- /dev/null
+++ b/src/test/resources/certs/truststore.p12
Binary files differ
diff --git a/src/test/resources/config/sidecar_invalid_client_auth.yaml b/src/test/resources/config/sidecar_invalid_client_auth.yaml
new file mode 100644
index 0000000..5ca9dfa
--- /dev/null
+++ b/src/test/resources/config/sidecar_invalid_client_auth.yaml
@@ -0,0 +1,135 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# Cassandra SideCar configuration file
+#
+cassandra_instances:
+  - id: 1
+    host: localhost1
+    port: 9042
+    username: cassandra
+    password: cassandra
+    data_dirs:
+      - /ccm/test/node1/data0
+      - /ccm/test/node1/data1
+    staging_dir: /ccm/test/node1/sstable-staging
+    jmx_host: 127.0.0.1
+    jmx_port: 7100
+    jmx_ssl_enabled: false
+  #    jmx_role:
+  #    jmx_role_password:
+  - id: 2
+    host: localhost2
+    port: 9042
+    username: cassandra
+    password: cassandra
+    data_dirs:
+      - /ccm/test/node2/data0
+      - /ccm/test/node2/data1
+    staging_dir: /ccm/test/node2/sstable-staging
+    jmx_host: 127.0.0.1
+    jmx_port: 7200
+    jmx_ssl_enabled: false
+  #    jmx_role:
+  #    jmx_role_password:
+  - id: 3
+    host: localhost3
+    port: 9042
+    username: cassandra
+    password: cassandra
+    data_dirs:
+      - /ccm/test/node3/data0
+      - /ccm/test/node3/data1
+    staging_dir: /ccm/test/node3/sstable-staging
+    jmx_host: 127.0.0.1
+    jmx_port: 7300
+    jmx_ssl_enabled: false
+#    jmx_role:
+#    jmx_role_password:
+
+sidecar:
+  host: 0.0.0.0
+  port: 9043
+  request_idle_timeout_millis: 300000 # this field expects integer value
+  request_timeout_millis: 300000
+  tcp_keep_alive: false
+  accept_backlog: 1024
+  throttle:
+    stream_requests_per_sec: 5000
+    delay_sec: 5
+    timeout_sec: 10
+  sstable_upload:
+    concurrent_upload_limit: 80
+    min_free_space_percent: 10
+  allowable_time_skew_in_minutes: 60
+  sstable_import:
+    poll_interval_millis: 100
+    cache:
+      expire_after_access_millis: 7200000 # 2 hours
+      maximum_size: 10000
+  worker_pools:
+    service:
+      name: "sidecar-worker-pool"
+      size: 20
+      max_execution_time_millis: 60000 # 60 seconds
+    internal:
+      name: "sidecar-internal-worker-pool"
+      size: 20
+      max_execution_time_millis: 900000 # 15 minutes
+  jmx:
+    max_retries: 3
+    retry_delay_millis: 200
+  traffic_shaping:
+    inbound_global_bandwidth_bps: 500
+    outbound_global_bandwidth_bps: 1500
+    peak_outbound_global_bandwidth_bps: 2000
+    max_delay_to_wait_millis: 2500
+    check_interval_for_stats_millis: 3000
+
+#
+# Enable SSL configuration (Disabled by default)
+#
+ssl:
+  enabled: true
+  use_openssl: false
+  handshake_timeout_sec: 25
+  client_auth: notvalid
+  keystore:
+    path: "path/to/keystore.p12"
+    password: password
+  truststore:
+    path: "path/to/truststore.p12"
+    password: password
+
+
+healthcheck:
+  poll_freq_millis: 30000
+
+cassandra_input_validation:
+  forbidden_keyspaces:
+    - system_schema
+    - system_traces
+    - system_distributed
+    - system
+    - system_auth
+    - system_views
+    - system_virtual_schema
+  allowed_chars_for_directory: "[a-zA-Z0-9_-]+"
+  allowed_chars_for_component_name: "[a-zA-Z0-9_-]+(.db|.cql|.json|.crc32|TOC.txt)"
+  allowed_chars_for_restricted_component_name: "[a-zA-Z0-9_-]+(.db|TOC.txt)"
diff --git a/src/test/resources/config/sidecar_missing_jmx.yaml b/src/test/resources/config/sidecar_missing_jmx.yaml
index ae37a11..6e8a6db 100644
--- a/src/test/resources/config/sidecar_missing_jmx.yaml
+++ b/src/test/resources/config/sidecar_missing_jmx.yaml
@@ -68,10 +68,19 @@
   port: 9043
   request_idle_timeout_millis: 300000 # this field expects integer value
   request_timeout_millis: 300000
+  tcp_keep_alive: false
+  accept_backlog: 1024
+  server_verticle_instances: 1
   throttle:
     stream_requests_per_sec: 5000
     delay_sec: 5
     timeout_sec: 10
+  traffic_shaping:
+    inbound_global_bandwidth_bps: 500
+    outbound_global_bandwidth_bps: 1500
+    peak_outbound_global_bandwidth_bps: 2000
+    max_delay_to_wait_millis: 2500
+    check_interval_for_stats_millis: 3000
   sstable_upload:
     concurrent_upload_limit: 80
     min_free_space_percent: 10
@@ -91,20 +100,26 @@
       name: "sidecar-internal-worker-pool"
       size: 20
       max_execution_time_millis: 900000 # 15 minutes
+
 #
 # Enable SSL configuration (Disabled by default)
 #
 #  ssl:
 #    enabled: true
+#    use_openssl: true
+#    handshake_timeout_sec: 10
+#    client_auth: NONE # valid options are NONE, REQUEST, REQUIRED
 #    keystore:
 #      path: "path/to/keystore.p12"
 #      password: password
+#      check_interval_sec: 300
 #    truststore:
 #      path: "path/to/truststore.p12"
 #      password: password
 
 
 healthcheck:
+  initial_delay_millis: 0
   poll_freq_millis: 30000
 
 cassandra_input_validation:
diff --git a/src/test/resources/config/sidecar_multiple_instances.yaml b/src/test/resources/config/sidecar_multiple_instances.yaml
index 886b942..c51cea0 100644
--- a/src/test/resources/config/sidecar_multiple_instances.yaml
+++ b/src/test/resources/config/sidecar_multiple_instances.yaml
@@ -68,10 +68,19 @@
   port: 9043
   request_idle_timeout_millis: 300000 # this field expects integer value
   request_timeout_millis: 300000
+  tcp_keep_alive: false
+  accept_backlog: 1024
+  server_verticle_instances: 1
   throttle:
     stream_requests_per_sec: 5000
     delay_sec: 5
     timeout_sec: 10
+  traffic_shaping:
+    inbound_global_bandwidth_bps: 500
+    outbound_global_bandwidth_bps: 1500
+    peak_outbound_global_bandwidth_bps: 2000
+    max_delay_to_wait_millis: 2500
+    check_interval_for_stats_millis: 3000
   sstable_upload:
     concurrent_upload_limit: 80
     min_free_space_percent: 10
@@ -94,20 +103,26 @@
   jmx:
     max_retries: 42
     retry_delay_millis: 1234
+
 #
 # Enable SSL configuration (Disabled by default)
 #
 #  ssl:
 #    enabled: true
+#    use_openssl: true
+#    handshake_timeout_sec: 10
+#    client_auth: NONE # valid options are NONE, REQUEST, REQUIRED
 #    keystore:
 #      path: "path/to/keystore.p12"
 #      password: password
+#      check_interval_sec: 300
 #    truststore:
 #      path: "path/to/truststore.p12"
 #      password: password
 
 
 healthcheck:
+  initial_delay_millis: 100
   poll_freq_millis: 30000
 
 cassandra_input_validation:
diff --git a/src/test/resources/config/sidecar_single_instance.yaml b/src/test/resources/config/sidecar_single_instance.yaml
index 8948660..f95fd57 100644
--- a/src/test/resources/config/sidecar_single_instance.yaml
+++ b/src/test/resources/config/sidecar_single_instance.yaml
@@ -18,16 +18,26 @@
 
 sidecar:
   host: 0.0.0.0
-  port: 9043
+  port: 0 # bind sever to the first available port
   request_idle_timeout_millis: 300000 # this field expects integer value
   request_timeout_millis: 300000
+  tcp_keep_alive: false
+  accept_backlog: 1024
+  server_verticle_instances: 2
   throttle:
     stream_requests_per_sec: 5000
     delay_sec: 5
     timeout_sec: 10
+  traffic_shaping:
+    inbound_global_bandwidth_bps: 500
+    outbound_global_bandwidth_bps: 1500
+    peak_outbound_global_bandwidth_bps: 2000
+    max_delay_to_wait_millis: 2500
+    check_interval_for_stats_millis: 3000
   sstable_upload:
     concurrent_upload_limit: 80
     min_free_space_percent: 10
+    # file_permissions: "rw-r--r--" # when not specified, the default file permissions are owner read & write, group & others read
   allowable_time_skew_in_minutes: 60
   sstable_import:
     poll_interval_millis: 100
@@ -42,22 +52,30 @@
     internal:
       name: "sidecar-internal-worker-pool"
       size: 20
-      max_execution_time_millis: 300000 # 5 minutes
+      max_execution_time_millis: 900000 # 15 minutes
+  jmx:
+    max_retries: 42
+    retry_delay_millis: 1234
 
 #
 # Enable SSL configuration (Disabled by default)
 #
 #  ssl:
 #    enabled: true
+#    use_openssl: true
+#    handshake_timeout_sec: 10
+#    client_auth: NONE # valid options are NONE, REQUEST, REQUIRED
 #    keystore:
 #      path: "path/to/keystore.p12"
 #      password: password
+#      check_interval_sec: 300
 #    truststore:
 #      path: "path/to/truststore.p12"
 #      password: password
 
 
 healthcheck:
+  initial_delay_millis: 100
   poll_freq_millis: 30000
 
 cassandra_input_validation:
diff --git a/src/test/resources/config/sidecar_ssl.yaml b/src/test/resources/config/sidecar_ssl.yaml
index fe03502..644f359 100644
--- a/src/test/resources/config/sidecar_ssl.yaml
+++ b/src/test/resources/config/sidecar_ssl.yaml
@@ -68,13 +68,23 @@
   port: 9043
   request_idle_timeout_millis: 300000 # this field expects integer value
   request_timeout_millis: 300000
+  tcp_keep_alive: false
+  accept_backlog: 1024
+  server_verticle_instances: 1
   throttle:
     stream_requests_per_sec: 5000
     delay_sec: 5
     timeout_sec: 10
+  traffic_shaping:
+    inbound_global_bandwidth_bps: 500
+    outbound_global_bandwidth_bps: 1500
+    peak_outbound_global_bandwidth_bps: 2000
+    max_delay_to_wait_millis: 2500
+    check_interval_for_stats_millis: 3000
   sstable_upload:
     concurrent_upload_limit: 80
     min_free_space_percent: 10
+    # file_permissions: "rw-r--r--" # when not specified, the default file permissions are owner read & write, group & others read
   allowable_time_skew_in_minutes: 60
   sstable_import:
     poll_interval_millis: 100
@@ -89,22 +99,30 @@
     internal:
       name: "sidecar-internal-worker-pool"
       size: 20
-      max_execution_time_millis: 300000 # 5 minutes
+      max_execution_time_millis: 900000 # 15 minutes
+  jmx:
+    max_retries: 3
+    retry_delay_millis: 200
 
 #
 # Enable SSL configuration (Disabled by default)
 #
 ssl:
   enabled: true
+  use_openssl: false
+  handshake_timeout_sec: 25
+  client_auth: REQUEST # valid options are NONE, REQUEST, REQUIRED
   keystore:
     path: "path/to/keystore.p12"
     password: password
+    check_interval_sec: 300
   truststore:
     path: "path/to/truststore.p12"
     password: password
 
 
 healthcheck:
+  initial_delay_millis: 100
   poll_freq_millis: 30000
 
 cassandra_input_validation:
diff --git a/vertx-client/build.gradle b/vertx-client/build.gradle
index d1d8516..494a886 100644
--- a/vertx-client/build.gradle
+++ b/vertx-client/build.gradle
@@ -49,12 +49,9 @@
     all*.exclude(group: 'ch.qos.logback')
 }
 
-// TODO: The vertx version differs from the version the server uses because the version used by the server does not
-//       support configuring daemon threads. There's a pending action on updating vertx's version for the server and
-//       keep them in sync.
 dependencies {
     api(project(':client'))
-    api(group: 'io.vertx', name: 'vertx-web-client', version: '4.4.1') {
+    api(group: 'io.vertx', name: 'vertx-web-client', version: "${project.vertxVersion}") {
         exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core'
     }
     implementation(group: 'io.netty', name: 'netty-tcnative-boringssl-static', version: '2.0.61.Final')  // for openSSL
@@ -77,7 +74,7 @@
 
     testImplementation(testFixtures(project(':client')))
 
-    testImplementation("io.vertx:vertx-junit5:4.4.1")
+    testImplementation("io.vertx:vertx-junit5:${project.vertxVersion}")
     testImplementation("org.assertj:assertj-core:3.24.2")
     testImplementation("org.junit.jupiter:junit-jupiter-api:${project.junitVersion}")
     testImplementation("org.junit.jupiter:junit-jupiter-params:${project.junitVersion}")