Introduce environment cache  (#139)

* Add url links between pages

*Modifications*

- fix texts
- add links between pages
- fix select issues

* Fix the dropdown list for environments

*Modifications*

Display the current environment at the top-right corner

* Introduce environment cache

*Motivation*

A environment might have multiple geo-replicated clusters to manage.
We need to make sure the backend route the http requests to the right clusters.

This pull request introduces the environment cache to cache the broker service urls
for different clusters in different environments.
diff --git a/build.gradle b/build.gradle
index 723e0e9..9de840f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -79,4 +79,5 @@
     compile group: 'org.powermock', name: 'powermock-api-mockito', version: apiMockitoVersion
     compile group: 'org.powermock', name: 'powermock-module-junit4', version: mockitoJunit4Version
     compileOnly group: 'org.projectlombok', name: 'lombok', version: lombokVersion
+    testCompile group: 'com.h2database', name: 'h2', version: h2databaseVersion
 }
diff --git a/front-end/src/lang/en.js b/front-end/src/lang/en.js
index d6c63da..dc014a0 100644
--- a/front-end/src/lang/en.js
+++ b/front-end/src/lang/en.js
@@ -1,4 +1,17 @@
 /*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*
  * Licensed under the Apache License, Version 2.0 (the 'License');
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
diff --git a/gradle.properties b/gradle.properties
index 90f3c91..a8d5e49 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -14,3 +14,4 @@
 apiMockitoVersion=1.7.1
 mockitoJunit4Version=1.7.1
 gsonVersion=2.8.2
+h2databaseVersion=1.4.199
diff --git a/src/main/java/com/manager/pulsar/controller/BrokerStatsController.java b/src/main/java/com/manager/pulsar/controller/BrokerStatsController.java
index 77bb952..fe5c1b5 100644
--- a/src/main/java/com/manager/pulsar/controller/BrokerStatsController.java
+++ b/src/main/java/com/manager/pulsar/controller/BrokerStatsController.java
@@ -13,9 +13,8 @@
  */
 package com.manager.pulsar.controller;
 
-import com.manager.pulsar.entity.EnvironmentsRepository;
 import com.manager.pulsar.service.BrokerStatsService;
-import com.manager.pulsar.utils.EnvironmentTools;
+import com.manager.pulsar.service.EnvironmentCacheService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiResponse;
@@ -43,7 +42,7 @@
     private BrokerStatsService brokerStatsService;
 
     @Autowired
-    private EnvironmentsRepository environmentsRepository;
+    private EnvironmentCacheService environmentCacheService;
 
     @Autowired
     private HttpServletRequest request;
@@ -56,7 +55,7 @@
     @RequestMapping(value = "/broker-stats/metrics", method =  RequestMethod.GET)
     public ResponseEntity<String> getBrokerStatsMetrics(
             @RequestParam() String broker) {
-        String requestHost = EnvironmentTools.getEnvironment(request, environmentsRepository);
+        String requestHost = environmentCacheService.getServiceUrl(request);
         String result = brokerStatsService.forwarBrokerStatsMetrics(broker, requestHost);
         return ResponseEntity.ok(result);
     }
@@ -69,7 +68,7 @@
     @RequestMapping(value = "/broker-stats/topics", method =  RequestMethod.GET)
     public ResponseEntity<String> getBrokerStatsTopics(
             @RequestParam() String broker) {
-        String requestHost = EnvironmentTools.getEnvironment(request, environmentsRepository);
+        String requestHost = environmentCacheService.getServiceUrl(request);
         String result = brokerStatsService.forwardBrokerStatsTopics(broker, requestHost);
         return ResponseEntity.ok(result);
     }
diff --git a/src/main/java/com/manager/pulsar/controller/BrokersController.java b/src/main/java/com/manager/pulsar/controller/BrokersController.java
index 4f562e7..d8613bc 100644
--- a/src/main/java/com/manager/pulsar/controller/BrokersController.java
+++ b/src/main/java/com/manager/pulsar/controller/BrokersController.java
@@ -13,10 +13,8 @@
  */
 package com.manager.pulsar.controller;
 
-import com.manager.pulsar.entity.BrokersRepository;
-import com.manager.pulsar.entity.EnvironmentsRepository;
 import com.manager.pulsar.service.BrokersService;
-import com.manager.pulsar.utils.EnvironmentTools;
+import com.manager.pulsar.service.EnvironmentCacheService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -49,14 +47,11 @@
     private BrokersService brokersService;
 
     @Autowired
-    private BrokersRepository brokersRepository;
-
-    @Autowired
-    private EnvironmentsRepository environmentsRepository;
-
-    @Autowired
     private HttpServletRequest request;
 
+    @Autowired
+    private EnvironmentCacheService environmentCacheService;
+
     @ApiOperation(value = "Get the list of existing brokers, support paging, the default is 10 per page")
     @ApiResponses({
             @ApiResponse(code = 200, message = "ok"),
@@ -73,8 +68,8 @@
             @Range(min = 1, max = 1000, message = "page_size is incorrect, should be greater than 0 and less than 1000.")
             Integer pageSize,
             @PathVariable String cluster) {
-        String requestHost = EnvironmentTools.getEnvironment(request, environmentsRepository);
-        Map<String, Object> result = brokersService.getBrokersList(pageNum, pageSize, cluster, requestHost);
+        String requestServiceUrl = environmentCacheService.getServiceUrl(request, cluster);
+        Map<String, Object> result = brokersService.getBrokersList(pageNum, pageSize, cluster, requestServiceUrl);
         return ResponseEntity.ok(result);
     }
 //
diff --git a/src/main/java/com/manager/pulsar/controller/ClustersController.java b/src/main/java/com/manager/pulsar/controller/ClustersController.java
index 3febc43..97e2704 100644
--- a/src/main/java/com/manager/pulsar/controller/ClustersController.java
+++ b/src/main/java/com/manager/pulsar/controller/ClustersController.java
@@ -15,9 +15,8 @@
 
 import com.manager.pulsar.entity.ClusterEntity;
 import com.manager.pulsar.entity.ClustersRepository;
-import com.manager.pulsar.entity.EnvironmentsRepository;
 import com.manager.pulsar.service.ClustersService;
-import com.manager.pulsar.utils.EnvironmentTools;
+import com.manager.pulsar.service.EnvironmentCacheService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -55,7 +54,7 @@
     private ClustersService clusterService;
 
     @Autowired
-    private EnvironmentsRepository environmentsRepository;
+    private EnvironmentCacheService environmentCacheService;
 
     @Autowired
     private HttpServletRequest request;
@@ -75,7 +74,7 @@
             @RequestParam(name="page_size", defaultValue = "10")
             @Range(min = 1, max = 1000, message = "page_size is incorrect, should be greater than 0 and less than 1000.")
                     Integer pageSize) {
-        String requestHost = EnvironmentTools.getEnvironment(request, environmentsRepository);
+        String requestHost = environmentCacheService.getServiceUrl(request);
         Map<String, Object> result = clusterService.getClustersList(pageNum, pageSize, requestHost);
 
         return ResponseEntity.ok(result);
diff --git a/src/main/java/com/manager/pulsar/controller/EnvironmentsController.java b/src/main/java/com/manager/pulsar/controller/EnvironmentsController.java
index 7de0d78..08ce902 100644
--- a/src/main/java/com/manager/pulsar/controller/EnvironmentsController.java
+++ b/src/main/java/com/manager/pulsar/controller/EnvironmentsController.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.Maps;
 import com.manager.pulsar.entity.EnvironmentEntity;
 import com.manager.pulsar.entity.EnvironmentsRepository;
+import com.manager.pulsar.service.EnvironmentCacheService;
 import com.manager.pulsar.utils.HttpUtil;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
@@ -49,6 +50,9 @@
     @Autowired
     private EnvironmentsRepository environmentsRepository;
 
+    @Autowired
+    private EnvironmentCacheService environmentCacheService;
+
     @ApiOperation(value = "Get the list of existing environments, support paging, the default is 10 per page")
     @ApiResponses({
             @ApiResponse(code = 200, message = "ok"),
@@ -104,6 +108,7 @@
             return ResponseEntity.ok(result);
         }
         environmentsRepository.save(environmentEntity);
+        environmentCacheService.reloadEnvironment(environmentEntity);
         result.put("message", "Add environment success");
         return ResponseEntity.ok(result);
     }
@@ -130,6 +135,7 @@
             return ResponseEntity.ok(result);
         }
         environmentsRepository.update(environmentEntity);
+        environmentCacheService.reloadEnvironment(environmentEntity);
         result.put("message", "Update environment success");
         return ResponseEntity.ok(result);
     }
diff --git a/src/main/java/com/manager/pulsar/controller/NamespacesController.java b/src/main/java/com/manager/pulsar/controller/NamespacesController.java
index 5b7d36c..ad86467 100644
--- a/src/main/java/com/manager/pulsar/controller/NamespacesController.java
+++ b/src/main/java/com/manager/pulsar/controller/NamespacesController.java
@@ -15,11 +15,10 @@
 
 import com.github.pagehelper.Page;
 import com.google.common.collect.Maps;
-import com.manager.pulsar.entity.EnvironmentsRepository;
 import com.manager.pulsar.entity.NamespaceEntity;
 import com.manager.pulsar.entity.NamespacesRepository;
+import com.manager.pulsar.service.EnvironmentCacheService;
 import com.manager.pulsar.service.NamespacesService;
-import com.manager.pulsar.utils.EnvironmentTools;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -57,7 +56,7 @@
     private NamespacesService namespacesService;
 
     @Autowired
-    private EnvironmentsRepository environmentsRepository;
+    private EnvironmentCacheService environmentCacheService;
 
     @Autowired
     private HttpServletRequest request;
@@ -102,7 +101,7 @@
             @RequestParam(name="page_size", defaultValue = "10")
             @Range(min = 1, max = 1000, message = "page_size is incorrect, should be greater than 0 and less than 1000.")
             Integer pageSize) {
-        String requestHost = EnvironmentTools.getEnvironment(request, environmentsRepository);
+        String requestHost = environmentCacheService.getServiceUrl(request);
         Map<String, Object> result = namespacesService.getNamespaceList(pageNum, pageSize, tenantOrNamespace, requestHost);
         return ResponseEntity.ok(result);
     }
diff --git a/src/main/java/com/manager/pulsar/controller/TenantsController.java b/src/main/java/com/manager/pulsar/controller/TenantsController.java
index e345a72..f296266 100644
--- a/src/main/java/com/manager/pulsar/controller/TenantsController.java
+++ b/src/main/java/com/manager/pulsar/controller/TenantsController.java
@@ -13,9 +13,8 @@
  */
 package com.manager.pulsar.controller;
 
-import com.manager.pulsar.entity.EnvironmentsRepository;
+import com.manager.pulsar.service.EnvironmentCacheService;
 import com.manager.pulsar.service.TenantsService;
-import com.manager.pulsar.utils.EnvironmentTools;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -43,33 +42,33 @@
 @Validated
 public class TenantsController {
 
-  @Autowired
-  private TenantsService tenantsService;
+    @Autowired
+    private TenantsService tenantsService;
 
     @Autowired
-    private EnvironmentsRepository environmentsRepository;
+    private EnvironmentCacheService environmentCacheService;
 
     @Autowired
     private HttpServletRequest request;
 
-  @ApiOperation(value = "Get the list of existing tenants, support paging, the default is 10 per page")
-  @ApiResponses({
-          @ApiResponse(code = 200, message = "ok"),
-          @ApiResponse(code = 404, message = "Not found"),
-          @ApiResponse(code = 500, message = "Internal server error")
-  })
-  @RequestMapping(value = "/tenants", method =  RequestMethod.GET)
-  public ResponseEntity<Map<String, Object>> getTenants(
-          @ApiParam(value = "page_num", defaultValue = "1", example = "1")
-          @RequestParam(name = "page_num", defaultValue = "1")
-          @Min(value = 1, message = "page_num is incorrect, should be greater than 0.")
-          Integer pageNum,
-          @ApiParam(value = "page_size", defaultValue = "10", example = "10")
-          @RequestParam(name="page_size", defaultValue = "10")
-          @Range(min = 1, max = 1000, message = "page_size is incorrect, should be greater than 0 and less than 1000.")
-          Integer pageSize) {
-      String requestHost = EnvironmentTools.getEnvironment(request, environmentsRepository);
-      Map<String, Object> result = tenantsService.getTenantsList(pageNum, pageSize, requestHost);
-    return ResponseEntity.ok(result);
-  }
+    @ApiOperation(value = "Get the list of existing tenants, support paging, the default is 10 per page")
+    @ApiResponses({
+        @ApiResponse(code = 200, message = "ok"),
+        @ApiResponse(code = 404, message = "Not found"),
+        @ApiResponse(code = 500, message = "Internal server error")
+    })
+    @RequestMapping(value = "/tenants", method =  RequestMethod.GET)
+    public ResponseEntity<Map<String, Object>> getTenants(
+        @ApiParam(value = "page_num", defaultValue = "1", example = "1")
+        @RequestParam(name = "page_num", defaultValue = "1")
+        @Min(value = 1, message = "page_num is incorrect, should be greater than 0.")
+            Integer pageNum,
+        @ApiParam(value = "page_size", defaultValue = "10", example = "10")
+        @RequestParam(name="page_size", defaultValue = "10")
+        @Range(min = 1, max = 1000, message = "page_size is incorrect, should be greater than 0 and less than 1000.")
+            Integer pageSize) {
+        String requestHost = environmentCacheService.getServiceUrl(request);
+        Map<String, Object> result = tenantsService.getTenantsList(pageNum, pageSize, requestHost);
+        return ResponseEntity.ok(result);
+    }
 }
diff --git a/src/main/java/com/manager/pulsar/controller/TopicsController.java b/src/main/java/com/manager/pulsar/controller/TopicsController.java
index f59fcb3..cdf382a 100644
--- a/src/main/java/com/manager/pulsar/controller/TopicsController.java
+++ b/src/main/java/com/manager/pulsar/controller/TopicsController.java
@@ -13,9 +13,8 @@
  */
 package com.manager.pulsar.controller;
 
-import com.manager.pulsar.entity.EnvironmentsRepository;
+import com.manager.pulsar.service.EnvironmentCacheService;
 import com.manager.pulsar.service.TopicsService;
-import com.manager.pulsar.utils.EnvironmentTools;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -48,7 +47,7 @@
     private TopicsService topicsService;
 
     @Autowired
-    private EnvironmentsRepository environmentsRepository;
+    private EnvironmentCacheService environmentCacheService;
 
     @Autowired
     private HttpServletRequest request;
@@ -74,7 +73,7 @@
             @ApiParam(value = "The name of namespace")
             @Size(min = 1, max = 255)
             @PathVariable String namespace) {
-        String requestHost = EnvironmentTools.getEnvironment(request, environmentsRepository);
+        String requestHost = environmentCacheService.getServiceUrl(request);
         Map<String, Object> result = topicsService.getTopicsList(pageNum, pageSize, tenant, namespace, requestHost);
         return result;
     }
diff --git a/src/main/java/com/manager/pulsar/service/EnvironmentCacheService.java b/src/main/java/com/manager/pulsar/service/EnvironmentCacheService.java
new file mode 100644
index 0000000..379ae99
--- /dev/null
+++ b/src/main/java/com/manager/pulsar/service/EnvironmentCacheService.java
@@ -0,0 +1,58 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.manager.pulsar.service;
+
+import com.manager.pulsar.entity.EnvironmentEntity;
+import javax.servlet.http.HttpServletRequest;
+
+public interface EnvironmentCacheService {
+
+    /**
+     * Return the service url for a given http request.
+     *
+     * @param request http request
+     * @return the service url that that http request should be forwarded to.
+     */
+    String getServiceUrl(HttpServletRequest request);
+
+    /**
+     * Return the service url for a given http request for a given cluster.
+     *
+     * @param request http request
+     * @param cluster cluster name
+     * @return the service url that the http request should be forwarded to.
+     */
+    String getServiceUrl(HttpServletRequest request, String cluster);
+
+    /**
+     * Return the service url for a given cluster at a given environment.
+     *
+     * @param environment environment name
+     * @param cluster cluster name
+     * @return the service url that the http request should be forwarded to.
+     */
+    String getServiceUrl(String environment, String cluster);
+
+    /**
+     * Refresh all the environments.
+     */
+    void reloadEnvironments();
+
+    /**
+     * Refresh the environment in the cache.
+     *
+     * @param environment environment entity.
+     */
+    void reloadEnvironment(EnvironmentEntity environment);
+}
diff --git a/src/main/java/com/manager/pulsar/service/impl/EnvironmentCacheServiceImpl.java b/src/main/java/com/manager/pulsar/service/impl/EnvironmentCacheServiceImpl.java
new file mode 100644
index 0000000..65b6cc3
--- /dev/null
+++ b/src/main/java/com/manager/pulsar/service/impl/EnvironmentCacheServiceImpl.java
@@ -0,0 +1,197 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.manager.pulsar.service.impl;
+
+import com.github.pagehelper.Page;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.manager.pulsar.entity.EnvironmentEntity;
+import com.manager.pulsar.entity.EnvironmentsRepository;
+import com.manager.pulsar.service.EnvironmentCacheService;
+import com.manager.pulsar.utils.HttpUtil;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import javax.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.pulsar.common.policies.data.ClusterData;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+/**
+ * A cache that caches environments.
+ */
+@Slf4j
+@Service
+public class EnvironmentCacheServiceImpl implements EnvironmentCacheService {
+
+    @Autowired
+    private EnvironmentsRepository environmentsRepository;
+
+    private final Map<String, Map<String, ClusterData>> environments;
+
+    public EnvironmentCacheServiceImpl() {
+        this.environments = new ConcurrentHashMap<>();
+    }
+
+    @Override
+    public String getServiceUrl(HttpServletRequest request) {
+        String cluster = request.getParameter("cluster");
+        return getServiceUrl(request, cluster);
+    }
+
+    @Override
+    public String getServiceUrl(HttpServletRequest request, String cluster) {
+        String environment = request.getHeader("environment");
+        return getServiceUrl(environment, cluster);
+    }
+
+    public String getServiceUrl(String environment, String cluster) {
+        if (null == cluster) {
+            // if there is no cluster is specified, forward the request to environment service url
+            Optional<EnvironmentEntity> environmentEntityOptional = environmentsRepository.findByName(environment);
+            EnvironmentEntity environmentEntity = environmentEntityOptional.get();
+            String directRequestHost = environmentEntity.getBroker();
+            return directRequestHost;
+        } else {
+            return getServiceUrl(environment, cluster, 0);
+        }
+    }
+
+    private String getServiceUrl(String environment, String cluster, int numReloads) {
+        log.info("Get service url from {} @ {} : numReloads = {}", environment, cluster, numReloads);
+        // if there is a cluster specified, lookup the cluster.
+        Map<String, ClusterData> clusters = environments.get(environment);
+        log.info("cluster : {}", clusters);
+        ClusterData clusterData;
+        if (null == clusters) {
+            clusterData = reloadCluster(environment, cluster);
+        } else {
+            clusterData = clusters.get(cluster);
+            if (clusterData == null) {
+                clusterData = reloadCluster(environment, cluster);
+            }
+        }
+
+        if (null == clusterData) {
+            // no environment and no cluster
+            throw new RuntimeException(
+                "No cluster '" + cluster + "' found in environment '" + environment + "'");
+        }
+        return clusterData.getServiceUrl();
+    }
+
+    private Map<String, String> jsonHeader() {
+        Map<String, String> header = Maps.newHashMap();
+        header.put("Content-Type", "application/json");
+        return header;
+    }
+
+    @Scheduled(
+        initialDelay = 0L,
+        fixedDelayString = "${cluster.cache.reload.interval.ms}")
+    @Override
+    public void reloadEnvironments() {
+        int pageNum = 0;
+        final int pageSize = 100;
+        Set<String> newEnvironments = new HashSet<>();
+        Page<EnvironmentEntity> environmentPage = environmentsRepository.getEnvironmentsList(pageNum, pageSize);
+        List<EnvironmentEntity> environmentList = environmentPage.getResult();
+        while (!environmentList.isEmpty()) {
+            environmentList.forEach(env -> {
+                reloadEnvironment(env);
+                newEnvironments.add(env.getName());
+            });
+            ++pageNum;
+            environmentPage = environmentsRepository.getEnvironmentsList(pageNum, pageSize);
+            environmentList = environmentPage.getResult();
+        }
+        log.info("Successfully reloaded environments : {}", newEnvironments);
+        Set<String> oldEnvironments = environments.keySet();
+        Set<String> goneEnvironments = Sets.difference(oldEnvironments, newEnvironments);
+        for (String env : goneEnvironments) {
+            environments.remove(env);
+            log.info("Removed cached environment {} since it is already deleted.", env);
+        }
+    }
+
+    private void reloadEnvironment(String environment) {
+        // if there is no clusters, lookup the clusters
+        EnvironmentEntity entity = environmentsRepository.findByName(environment).get();
+        reloadEnvironment(entity);
+    }
+
+    public void reloadEnvironment(EnvironmentEntity environment) {
+        Gson gson = new Gson();
+        String result = HttpUtil.doGet(
+            environment.getBroker() + "/admin/v2/clusters",
+            jsonHeader()
+        );
+        List<String> clustersList =
+            gson.fromJson(result, new TypeToken<List<String>>(){}.getType());
+        log.info("Reload cluster list for environment {} : {}", environment.getName(), clustersList);
+        Set<String> newClusters = Sets.newHashSet(clustersList);
+        Map<String, ClusterData> clusterDataMap = environments.computeIfAbsent(
+            environment.getName(),
+            (e) -> new ConcurrentHashMap<>());
+        Set<String> oldClusters = clusterDataMap.keySet();
+        Set<String> goneClusters = Sets.difference(oldClusters, newClusters);
+        for (String cluster : goneClusters) {
+            log.info("Remove cluster {} from environment {}.", cluster, environment.getName());
+            clusterDataMap.remove(cluster);
+        }
+        for (String cluster : clustersList) {
+            reloadCluster(environment, cluster);
+        }
+    }
+
+    private ClusterData reloadCluster(String environment, String cluster) {
+        // if there is no clusters, lookup the clusters
+        return environmentsRepository.findByName(environment).map(env ->
+            reloadCluster(env, cluster)
+        ).orElse(null);
+    }
+
+    private ClusterData reloadCluster(EnvironmentEntity environment, String cluster) {
+        log.info("Reloading cluster data for cluster {} @ environment {} ...",
+            cluster, environment.getName());
+        Gson gson = new Gson();
+        String clusterInfoUrl = environment.getBroker() + "/admin/v2/clusters/" + cluster;
+        String result = HttpUtil.doGet(
+            clusterInfoUrl,
+            jsonHeader()
+        );
+        if (null == result) {
+            // fail to fetch the cluster data or the cluster is not found
+            return null;
+        }
+        log.info("Loaded cluster data for cluster {} @ environment {} from {} : {}",
+            cluster, environment.getName(), clusterInfoUrl, result);
+        ClusterData clusterData = gson.fromJson(result, ClusterData.class);
+        Map<String, ClusterData> clusters = environments.computeIfAbsent(
+            environment.getName(),
+            (e) -> new ConcurrentHashMap<>());
+        clusters.put(cluster, clusterData);
+        log.info("Successfully loaded cluster data for cluster {} @ environment {} : {}",
+            cluster, environment.getName(), clusterData);
+        return clusterData;
+    }
+
+}
diff --git a/src/main/java/com/manager/pulsar/utils/EnvironmentTools.java b/src/main/java/com/manager/pulsar/utils/EnvironmentTools.java
deleted file mode 100644
index d5c904c..0000000
--- a/src/main/java/com/manager/pulsar/utils/EnvironmentTools.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.manager.pulsar.utils;
-
-import com.manager.pulsar.entity.EnvironmentEntity;
-import com.manager.pulsar.entity.EnvironmentsRepository;
-
-import javax.servlet.http.HttpServletRequest;
-import java.util.Optional;
-
-public class EnvironmentTools {
-
-    public static String getEnvironment(HttpServletRequest request, EnvironmentsRepository environmentsRepository) {
-        String environment = request.getHeader("environment");
-        Optional<EnvironmentEntity> environmentEntityOptional = environmentsRepository.findByName(environment);
-        EnvironmentEntity environmentEntity = environmentEntityOptional.get();
-        String directRequestHost = environmentEntity.getBroker();
-        return directRequestHost;
-    }
-}
diff --git a/src/main/java/com/manager/pulsar/utils/HttpUtil.java b/src/main/java/com/manager/pulsar/utils/HttpUtil.java
index e8c78cd..4054e4f 100644
--- a/src/main/java/com/manager/pulsar/utils/HttpUtil.java
+++ b/src/main/java/com/manager/pulsar/utils/HttpUtil.java
@@ -80,8 +80,8 @@
             } else {
                 request.abort();
             }
-        } catch (Exception e) {
-            log.error("http request exception:{}",e.getMessage());
+        } catch (Throwable cause) {
+            log.error("http request exception:{}", cause.getMessage());
         } finally {
             try{
                 if (response != null) {
diff --git a/src/main/java/com/manager/pulsar/zuul/EnvirmentForward.java b/src/main/java/com/manager/pulsar/zuul/EnvirmentForward.java
index 80fedda..6b5714a 100644
--- a/src/main/java/com/manager/pulsar/zuul/EnvirmentForward.java
+++ b/src/main/java/com/manager/pulsar/zuul/EnvirmentForward.java
@@ -13,8 +13,7 @@
  */
 package com.manager.pulsar.zuul;
 
-import com.manager.pulsar.entity.EnvironmentEntity;
-import com.manager.pulsar.entity.EnvironmentsRepository;
+import com.manager.pulsar.service.EnvironmentCacheService;
 import com.netflix.zuul.ZuulFilter;
 import com.netflix.zuul.context.RequestContext;
 import org.slf4j.Logger;
@@ -26,7 +25,6 @@
 import javax.servlet.http.HttpServletRequest;
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.util.Optional;
 
 import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
 import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.REQUEST_URI_KEY;
@@ -39,9 +37,8 @@
 
     private static final Logger log = LoggerFactory.getLogger(EnvirmentForward.class);
 
-
     @Autowired
-    private EnvironmentsRepository environmentsRepository;
+    private EnvironmentCacheService environmentCacheService;
 
     @Override
     public String filterType() {
@@ -79,16 +76,16 @@
             return null;
         }
         String environment = request.getHeader("environment");
-        Optional<EnvironmentEntity> entityOptional = environmentsRepository.findByName(environment);
-        if (entityOptional.isPresent()) {
-            EnvironmentEntity environmentEntity = entityOptional.get();
-            String broker = environmentEntity.getBroker();
-            ctx.put(REQUEST_URI_KEY, request.getRequestURI());
-            try {
-                ctx.setRouteHost(new URL(broker));
-            } catch(MalformedURLException mue) {
-                log.error("Route forward to {} path {} error: {}", broker, request.getRequestURI(), mue.getMessage());
-            }
+        if (null == environment) {
+            return null;
+        }
+        String serviceUrl = environmentCacheService.getServiceUrl(request);
+        ctx.put(REQUEST_URI_KEY, request.getRequestURI());
+        try {
+            ctx.setRouteHost(new URL(serviceUrl));
+        } catch (MalformedURLException e) {
+            log.error("Route forward to {} path {} error: {}",
+                serviceUrl, request.getRequestURI(), e.getMessage());
         }
         return null;
     }
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index fb5a371..26afae7 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -68,4 +68,7 @@
 bookie.enable=false
 
 redirect.host=localhost
-redirect.port=9527
\ No newline at end of file
+redirect.port=9527
+
+# cluster data reload
+cluster.cache.reload.interval.ms=60000        
\ No newline at end of file
diff --git a/src/test/java/com/manager/pulsar/PulsarManagerApplicationTests.java b/src/test/java/com/manager/pulsar/PulsarManagerApplicationTests.java
index 97aa32d..105022a 100644
--- a/src/test/java/com/manager/pulsar/PulsarManagerApplicationTests.java
+++ b/src/test/java/com/manager/pulsar/PulsarManagerApplicationTests.java
@@ -13,13 +13,21 @@
  */
 package com.manager.pulsar;
 
+import com.manager.pulsar.profiles.SqliteDBTestProfile;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.context.junit4.SpringRunner;
 
 @RunWith(SpringRunner.class)
-@SpringBootTest
+@SpringBootTest(
+    classes = {
+        PulsarManagerApplication.class,
+        SqliteDBTestProfile.class
+    }
+)
+@ActiveProfiles("test")
 public class PulsarManagerApplicationTests {
 
 	@Test
diff --git a/src/test/java/com/manager/pulsar/dao/BrokersRepositoryImplTest.java b/src/test/java/com/manager/pulsar/dao/BrokersRepositoryImplTest.java
index 13935f6..eab1174 100644
--- a/src/test/java/com/manager/pulsar/dao/BrokersRepositoryImplTest.java
+++ b/src/test/java/com/manager/pulsar/dao/BrokersRepositoryImplTest.java
@@ -14,28 +14,35 @@
 package com.manager.pulsar.dao;
 
 import com.github.pagehelper.Page;
+import com.manager.pulsar.PulsarManagerApplication;
 import com.manager.pulsar.entity.BrokerEntity;
 import com.manager.pulsar.entity.BrokersRepository;
+import com.manager.pulsar.profiles.SqliteDBTestProfile;
+import java.util.Optional;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.context.junit4.SpringRunner;
 
-import java.util.Optional;
-
 /**
  * Brokers crud test.
  */
 @RunWith(SpringRunner.class)
-@SpringBootTest
+@SpringBootTest(
+    classes = {
+        PulsarManagerApplication.class,
+        SqliteDBTestProfile.class
+    }
+)
+@ActiveProfiles("test")
 public class BrokersRepositoryImplTest {
 
     @Autowired
     private BrokersRepository brokersRepository;
 
-
     private void initBrokerEntity(BrokerEntity brokersEntity) {
         brokersEntity.setBrokerId(1);
         brokersEntity.setBroker("test-broker");
diff --git a/src/test/java/com/manager/pulsar/dao/BundlesRespositoryImplTest.java b/src/test/java/com/manager/pulsar/dao/BundlesRespositoryImplTest.java
index 012f1ce..330a0cd 100644
--- a/src/test/java/com/manager/pulsar/dao/BundlesRespositoryImplTest.java
+++ b/src/test/java/com/manager/pulsar/dao/BundlesRespositoryImplTest.java
@@ -15,20 +15,29 @@
 
 
 import com.github.pagehelper.Page;
+import com.manager.pulsar.PulsarManagerApplication;
 import com.manager.pulsar.entity.BundleEntity;
 import com.manager.pulsar.entity.BundlesRepository;
+import com.manager.pulsar.profiles.SqliteDBTestProfile;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.context.junit4.SpringRunner;
 
 /**
  * Bundles crud test.
  */
 @RunWith(SpringRunner.class)
-@SpringBootTest
+@SpringBootTest(
+    classes = {
+        PulsarManagerApplication.class,
+        SqliteDBTestProfile.class
+    }
+)
+@ActiveProfiles("test")
 public class BundlesRespositoryImplTest {
 
     @Autowired
diff --git a/src/test/java/com/manager/pulsar/dao/ClustersRepositoryImplTest.java b/src/test/java/com/manager/pulsar/dao/ClustersRepositoryImplTest.java
index 23c5cb2..78d9230 100644
--- a/src/test/java/com/manager/pulsar/dao/ClustersRepositoryImplTest.java
+++ b/src/test/java/com/manager/pulsar/dao/ClustersRepositoryImplTest.java
@@ -14,14 +14,17 @@
 package com.manager.pulsar.dao;
 
 import com.github.pagehelper.Page;
+import com.manager.pulsar.PulsarManagerApplication;
 import com.manager.pulsar.entity.ClusterEntity;
 import com.manager.pulsar.entity.ClustersRepository;
+import com.manager.pulsar.profiles.SqliteDBTestProfile;
 import org.apache.pulsar.shade.com.google.gson.Gson;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.context.junit4.SpringRunner;
 
 import java.util.HashSet;
@@ -29,7 +32,13 @@
 import java.util.Optional;
 
 @RunWith(SpringRunner.class)
-@SpringBootTest
+@SpringBootTest(
+    classes = {
+        PulsarManagerApplication.class,
+        SqliteDBTestProfile.class
+    }
+)
+@ActiveProfiles("test")
 public class ClustersRepositoryImplTest {
 
     @Autowired
diff --git a/src/test/java/com/manager/pulsar/dao/EnvironmentsRepositoryImplTest.java b/src/test/java/com/manager/pulsar/dao/EnvironmentsRepositoryImplTest.java
index d6432ba..e3f188a 100644
--- a/src/test/java/com/manager/pulsar/dao/EnvironmentsRepositoryImplTest.java
+++ b/src/test/java/com/manager/pulsar/dao/EnvironmentsRepositoryImplTest.java
@@ -14,19 +14,28 @@
 package com.manager.pulsar.dao;
 
 import com.github.pagehelper.Page;
+import com.manager.pulsar.PulsarManagerApplication;
 import com.manager.pulsar.entity.EnvironmentEntity;
 import com.manager.pulsar.entity.EnvironmentsRepository;
+import com.manager.pulsar.profiles.SqliteDBTestProfile;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.context.junit4.SpringRunner;
 
 import java.util.Optional;
 
 @RunWith(SpringRunner.class)
-@SpringBootTest
+@SpringBootTest(
+    classes = {
+        PulsarManagerApplication.class,
+        SqliteDBTestProfile.class
+    }
+)
+@ActiveProfiles("test")
 public class EnvironmentsRepositoryImplTest {
 
     @Autowired
diff --git a/src/test/java/com/manager/pulsar/dao/NamespacesRepositoryImplTest.java b/src/test/java/com/manager/pulsar/dao/NamespacesRepositoryImplTest.java
index 737fcad..e513977 100644
--- a/src/test/java/com/manager/pulsar/dao/NamespacesRepositoryImplTest.java
+++ b/src/test/java/com/manager/pulsar/dao/NamespacesRepositoryImplTest.java
@@ -14,10 +14,12 @@
 package com.manager.pulsar.dao;
 
 import com.github.pagehelper.Page;
+import com.manager.pulsar.PulsarManagerApplication;
 import com.manager.pulsar.entity.NamespaceEntity;
 import com.manager.pulsar.entity.NamespacesRepository;
 import com.manager.pulsar.entity.TenantEntity;
 import com.manager.pulsar.entity.TenantsRepository;
+import com.manager.pulsar.profiles.SqliteDBTestProfile;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -25,10 +27,17 @@
 import org.junit.runner.RunWith;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.context.junit4.SpringRunner;
 
 @RunWith(SpringRunner.class)
-@SpringBootTest
+@SpringBootTest(
+    classes = {
+        PulsarManagerApplication.class,
+        SqliteDBTestProfile.class
+    }
+)
+@ActiveProfiles("test")
 public class NamespacesRepositoryImplTest {
 
     @Autowired
diff --git a/src/test/java/com/manager/pulsar/dao/TenantsRepositoryImplTest.java b/src/test/java/com/manager/pulsar/dao/TenantsRepositoryImplTest.java
index f312507..aae9d40 100644
--- a/src/test/java/com/manager/pulsar/dao/TenantsRepositoryImplTest.java
+++ b/src/test/java/com/manager/pulsar/dao/TenantsRepositoryImplTest.java
@@ -14,19 +14,28 @@
 package com.manager.pulsar.dao;
 
 import com.github.pagehelper.Page;
+import com.manager.pulsar.PulsarManagerApplication;
 import com.manager.pulsar.entity.TenantEntity;
 import com.manager.pulsar.entity.TenantsRepository;
+import com.manager.pulsar.profiles.SqliteDBTestProfile;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.context.junit4.SpringRunner;
 
 import java.util.List;
 
 @RunWith(SpringRunner.class)
-@SpringBootTest
+@SpringBootTest(
+    classes = {
+        PulsarManagerApplication.class,
+        SqliteDBTestProfile.class
+    }
+)
+@ActiveProfiles("test")
 public class TenantsRepositoryImplTest {
 
     @Autowired
diff --git a/src/test/java/com/manager/pulsar/profiles/SqliteDBTestProfile.java b/src/test/java/com/manager/pulsar/profiles/SqliteDBTestProfile.java
new file mode 100644
index 0000000..b63a266
--- /dev/null
+++ b/src/test/java/com/manager/pulsar/profiles/SqliteDBTestProfile.java
@@ -0,0 +1,49 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.manager.pulsar.profiles;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import javax.sql.DataSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+@Configuration
+@EnableTransactionManagement
+public class SqliteDBTestProfile {
+
+    private static final Logger logger = LoggerFactory.getLogger(SqliteDBTestProfile.class);
+
+    @Bean
+    @Profile("test")
+    public DataSource dataSource() throws IOException {
+        Path tempFile = Files.createTempFile("test-pulsar-manager-db", "sqlite");
+        tempFile.toFile().deleteOnExit();
+        String sqliteDb = "jdbc:sqlite:" + tempFile.toFile().getAbsolutePath();
+        logger.info("Created a temp sqlite db for testing : {}", sqliteDb);
+
+        DriverManagerDataSource dataSource = new DriverManagerDataSource();
+        dataSource.setDriverClassName("org.sqlite.JDBC");
+        dataSource.setUrl(sqliteDb);
+
+        return dataSource;
+    }
+
+}
diff --git a/src/test/java/com/manager/pulsar/service/BookiesServiceImplTest.java b/src/test/java/com/manager/pulsar/service/BookiesServiceImplTest.java
index f814e0c..8e1c655 100644
--- a/src/test/java/com/manager/pulsar/service/BookiesServiceImplTest.java
+++ b/src/test/java/com/manager/pulsar/service/BookiesServiceImplTest.java
@@ -14,6 +14,8 @@
 package com.manager.pulsar.service;
 
 import com.google.common.collect.Maps;
+import com.manager.pulsar.PulsarManagerApplication;
+import com.manager.pulsar.profiles.SqliteDBTestProfile;
 import com.manager.pulsar.utils.HttpUtil;
 import org.junit.Assert;
 import org.junit.Test;
@@ -25,6 +27,7 @@
 import org.powermock.modules.junit4.PowerMockRunnerDelegate;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.context.TestPropertySource;
 import org.springframework.test.context.junit4.SpringRunner;
 
@@ -35,7 +38,13 @@
 @PowerMockIgnore( {"javax.management.*", "javax.net.ssl.*"})
 @PrepareForTest(HttpUtil.class)
 @TestPropertySource(locations= "classpath:test-bookie.properties")
-@SpringBootTest
+@SpringBootTest(
+    classes = {
+        PulsarManagerApplication.class,
+        SqliteDBTestProfile.class
+    }
+)
+@ActiveProfiles("test")
 public class BookiesServiceImplTest {
 
     @Autowired
diff --git a/src/test/java/com/manager/pulsar/service/BrokersServiceImplTest.java b/src/test/java/com/manager/pulsar/service/BrokersServiceImplTest.java
index a63cf59..796342d 100644
--- a/src/test/java/com/manager/pulsar/service/BrokersServiceImplTest.java
+++ b/src/test/java/com/manager/pulsar/service/BrokersServiceImplTest.java
@@ -13,12 +13,12 @@
  */
 package com.manager.pulsar.service;
 
-
 import com.google.common.collect.Maps;
+import com.manager.pulsar.PulsarManagerApplication;
 import com.manager.pulsar.entity.EnvironmentEntity;
-import com.manager.pulsar.entity.EnvironmentsRepository;
-import com.manager.pulsar.utils.EnvironmentTools;
+import com.manager.pulsar.profiles.SqliteDBTestProfile;
 import com.manager.pulsar.utils.HttpUtil;
+import java.util.Map;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -29,19 +29,20 @@
 import org.powermock.modules.junit4.PowerMockRunnerDelegate;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.context.junit4.SpringRunner;
-import org.springframework.test.web.servlet.MockMvc;
-
-import javax.servlet.http.HttpServletRequest;
-import java.util.Map;
-
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 
 @RunWith(PowerMockRunner.class)
 @PowerMockRunnerDelegate(SpringRunner.class)
 @PowerMockIgnore( {"javax.management.*", "javax.net.ssl.*"})
 @PrepareForTest(HttpUtil.class)
-@SpringBootTest
+@SpringBootTest(
+    classes = {
+        PulsarManagerApplication.class,
+        SqliteDBTestProfile.class
+    }
+)
+@ActiveProfiles("test")
 public class BrokersServiceImplTest {
 
     @Autowired
diff --git a/src/test/java/com/manager/pulsar/service/ClustersServiceImplTest.java b/src/test/java/com/manager/pulsar/service/ClustersServiceImplTest.java
index 72799e5..31ad346 100644
--- a/src/test/java/com/manager/pulsar/service/ClustersServiceImplTest.java
+++ b/src/test/java/com/manager/pulsar/service/ClustersServiceImplTest.java
@@ -14,6 +14,8 @@
 package com.manager.pulsar.service;
 
 import com.google.common.collect.Maps;
+import com.manager.pulsar.PulsarManagerApplication;
+import com.manager.pulsar.profiles.SqliteDBTestProfile;
 import com.manager.pulsar.utils.HttpUtil;
 import org.junit.Assert;
 import org.junit.Test;
@@ -25,6 +27,7 @@
 import org.powermock.modules.junit4.PowerMockRunnerDelegate;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.context.junit4.SpringRunner;
 
 import java.util.Map;
@@ -33,7 +36,13 @@
 @PowerMockRunnerDelegate(SpringRunner.class)
 @PowerMockIgnore( {"javax.management.*", "javax.net.ssl.*"})
 @PrepareForTest(HttpUtil.class)
-@SpringBootTest
+@SpringBootTest(
+    classes = {
+        PulsarManagerApplication.class,
+        SqliteDBTestProfile.class
+    }
+)
+@ActiveProfiles("test")
 public class ClustersServiceImplTest {
 
     @Autowired
diff --git a/src/test/java/com/manager/pulsar/service/EnvironmentCacheServiceImplTest.java b/src/test/java/com/manager/pulsar/service/EnvironmentCacheServiceImplTest.java
new file mode 100644
index 0000000..377916e
--- /dev/null
+++ b/src/test/java/com/manager/pulsar/service/EnvironmentCacheServiceImplTest.java
@@ -0,0 +1,250 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.manager.pulsar.service;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.anyMap;
+import static org.mockito.Matchers.eq;
+
+import com.google.gson.Gson;
+import com.manager.pulsar.PulsarManagerApplication;
+import com.manager.pulsar.entity.EnvironmentEntity;
+import com.manager.pulsar.entity.EnvironmentsRepository;
+import com.manager.pulsar.profiles.SqliteDBTestProfile;
+import com.manager.pulsar.utils.HttpUtil;
+import java.util.NoSuchElementException;
+import org.apache.pulsar.common.policies.data.ClusterData;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PowerMockIgnore;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+import org.powermock.modules.junit4.PowerMockRunnerDelegate;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+
+/**
+ * Unit test {@link EnvironmentCacheService}.
+ */
+@RunWith(PowerMockRunner.class)
+@PowerMockRunnerDelegate(SpringRunner.class)
+@PowerMockIgnore( {"javax.management.*", "javax.net.ssl.*"})
+@PrepareForTest(HttpUtil.class)
+@SpringBootTest(
+    classes = {
+        PulsarManagerApplication.class,
+        SqliteDBTestProfile.class
+    }
+)
+@ActiveProfiles("test")
+public class EnvironmentCacheServiceImplTest {
+
+    @Autowired
+    private EnvironmentsRepository environmentsRepository;
+
+    @Autowired
+    private EnvironmentCacheService environmentCache;
+
+    private EnvironmentEntity environment1;
+    private EnvironmentEntity environment2;
+    private EnvironmentEntity emptyEnvironment;
+    private final String cluster1_0_name = "cluster1_0";
+    private ClusterData cluster1_0;
+    private final String cluster2_0_name = "cluster2_0";
+    private ClusterData cluster2_0;
+    private final String cluster2_1_name = "cluster2_1";
+    private ClusterData cluster2_1;
+
+    @Before
+    public void setup() {
+        // setup 3 environments
+        environment1 = new EnvironmentEntity();
+        environment1.setBroker("http://cluster1_0:8080");
+        environment1.setName("environment1");
+        environment2 = new EnvironmentEntity();
+        environment2.setBroker("http://cluster2_0:8080");
+        environment2.setName("environment2");
+        emptyEnvironment = new EnvironmentEntity();
+        emptyEnvironment.setName("emptyEnvironment");
+        emptyEnvironment.setBroker("http://empty_env:8080");
+
+        // setup 3 clusters
+        cluster1_0 = new ClusterData();
+        cluster1_0.setServiceUrl("http://cluster1_0:8080");
+
+        cluster2_0 = new ClusterData();
+        cluster2_0.setServiceUrl("http://cluster2_0:8080");
+
+        cluster2_1 = new ClusterData();
+        cluster2_1.setServiceUrl("http://cluster2_1:8080");
+
+        PowerMockito.mockStatic(HttpUtil.class);
+        // empty environment
+        PowerMockito.when(HttpUtil.doGet(
+            eq(emptyEnvironment.getBroker() + "/admin/v2/clusters"),
+            anyMap()
+        )).thenReturn("[]");
+
+        // environment 1
+        PowerMockito.when(HttpUtil.doGet(
+            eq(cluster1_0.getServiceUrl() + "/admin/v2/clusters"),
+            anyMap()
+        )).thenReturn("[\""+ cluster1_0_name + "\"]");
+        PowerMockito.when(HttpUtil.doGet(
+            eq(cluster1_0.getServiceUrl() + "/admin/v2/clusters/" + cluster1_0_name),
+            anyMap()
+        )).thenReturn(new Gson().toJson(cluster1_0));
+
+        // environment 2
+        PowerMockito.when(HttpUtil.doGet(
+            eq(cluster2_0.getServiceUrl() + "/admin/v2/clusters"),
+            anyMap()
+        )).thenReturn("[\""+ cluster2_0_name + "\", \"" + cluster2_1_name + "\"]");
+        PowerMockito.when(HttpUtil.doGet(
+            eq(cluster2_0.getServiceUrl() + "/admin/v2/clusters/" + cluster2_0_name),
+            anyMap()
+        )).thenReturn(new Gson().toJson(cluster2_0));
+        PowerMockito.when(HttpUtil.doGet(
+            eq(cluster2_0.getServiceUrl() + "/admin/v2/clusters/" + cluster2_1_name),
+            anyMap()
+        )).thenReturn(new Gson().toJson(cluster2_1));
+    }
+
+    @After
+    public void teardown() {
+        environmentsRepository.remove(environment1.getName());
+        environmentsRepository.remove(environment2.getName());
+        environmentsRepository.remove(emptyEnvironment.getName());
+    }
+
+    @Test
+    public void testEmptyEnvironments() {
+        environmentCache.reloadEnvironments();
+
+        try {
+            environmentCache.getServiceUrl(environment1.getName(), null);
+            fail("Should fail to get service url if environments is empty");
+        } catch (NoSuchElementException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testEmptyEnvironment() {
+        environmentsRepository.save(emptyEnvironment);
+
+        try {
+            environmentCache.getServiceUrl(emptyEnvironment.getName(), cluster1_0_name);
+            fail("Should fail to get service url if environments is empty");
+        } catch (RuntimeException e) {
+            // expected
+            assertEquals(
+                "No cluster '" + cluster1_0_name + "' found in environment '"
+                    + emptyEnvironment.getName() + "'",
+                e.getMessage());
+        }
+    }
+
+    @Test
+    public void testReloadEnvironments() {
+        environmentsRepository.save(emptyEnvironment);
+        environmentsRepository.save(environment1);
+        environmentsRepository.save(environment2);
+
+        // without cluster
+
+        assertEquals(cluster1_0.getServiceUrl(),
+            environmentCache.getServiceUrl(environment1.getName(), null));
+        assertEquals(cluster2_0.getServiceUrl(),
+            environmentCache.getServiceUrl(environment2.getName(), null));
+
+        // with cluster
+
+        assertEquals(cluster1_0.getServiceUrl(),
+            environmentCache.getServiceUrl(environment1.getName(), cluster1_0_name));
+        assertEquals(cluster2_0.getServiceUrl(),
+            environmentCache.getServiceUrl(environment2.getName(), cluster2_0_name));
+        assertEquals(cluster2_1.getServiceUrl(),
+            environmentCache.getServiceUrl(environment2.getName(), cluster2_1_name));
+    }
+
+    @Test
+    public void testReloadEnvironmentsAddNewEnvironmentsAndRemoveOldEnvironments() {
+        environmentsRepository.save(environment1);
+
+        environmentCache.reloadEnvironments();
+        assertEquals(cluster1_0.getServiceUrl(),
+            environmentCache.getServiceUrl(environment1.getName(), null));
+        try {
+            environmentCache.getServiceUrl(environment2.getName(), null);
+            fail("Should fail to get service url if environments is empty");
+        } catch (NoSuchElementException e) {
+            // expected
+        }
+
+        environmentsRepository.save(environment2);
+        environmentsRepository.remove(environment1.getName());
+        environmentCache.reloadEnvironments();
+
+        assertEquals(cluster2_0.getServiceUrl(),
+            environmentCache.getServiceUrl(environment2.getName(), null));
+        try {
+            environmentCache.getServiceUrl(environment1.getName(), null);
+            fail("Should fail to get service url if environments is empty");
+        } catch (NoSuchElementException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testReloadEnvironmentsAddNewClusterAndRemoveOldCluster() {
+        environmentsRepository.save(environment2);
+        environmentCache.reloadEnvironments();
+        assertEquals(cluster2_0.getServiceUrl(),
+            environmentCache.getServiceUrl(environment2.getName(), cluster2_0_name));
+        assertEquals(cluster2_1.getServiceUrl(),
+            environmentCache.getServiceUrl(environment2.getName(), cluster2_1_name));
+
+        PowerMockito.when(HttpUtil.doGet(
+            eq(cluster2_0.getServiceUrl() + "/admin/v2/clusters"),
+            anyMap()
+        )).thenReturn("[\""+ cluster2_0_name + "\"]");
+        PowerMockito.when(HttpUtil.doGet(
+            eq(cluster2_0.getServiceUrl() + "/admin/v2/clusters/" + cluster2_1_name),
+            anyMap()
+        )).thenReturn(null);
+
+        environmentCache.reloadEnvironments();
+        assertEquals(cluster2_0.getServiceUrl(),
+            environmentCache.getServiceUrl(environment2.getName(), cluster2_0_name));
+        try {
+            assertEquals(cluster2_1.getServiceUrl(),
+                environmentCache.getServiceUrl(environment2.getName(), cluster2_1_name));
+            fail("Should fail to get service url if cluster is not found");
+        } catch (RuntimeException e) {
+            // expected
+            assertEquals(
+                "No cluster '" + cluster2_1_name + "' found in environment '"
+                    + environment2.getName() + "'",
+                e.getMessage());
+        }
+    }
+
+}
diff --git a/src/test/java/com/manager/pulsar/service/NamespacesServiceImplTest.java b/src/test/java/com/manager/pulsar/service/NamespacesServiceImplTest.java
index c660cf1..a72fe1c 100644
--- a/src/test/java/com/manager/pulsar/service/NamespacesServiceImplTest.java
+++ b/src/test/java/com/manager/pulsar/service/NamespacesServiceImplTest.java
@@ -14,6 +14,8 @@
 package com.manager.pulsar.service;
 
 import com.google.common.collect.Maps;
+import com.manager.pulsar.PulsarManagerApplication;
+import com.manager.pulsar.profiles.SqliteDBTestProfile;
 import com.manager.pulsar.utils.HttpUtil;
 import org.junit.Assert;
 import org.junit.Test;
@@ -25,6 +27,7 @@
 import org.powermock.modules.junit4.PowerMockRunnerDelegate;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.context.junit4.SpringRunner;
 
 import java.util.Map;
@@ -33,7 +36,13 @@
 @PowerMockRunnerDelegate(SpringRunner.class)
 @PowerMockIgnore( {"javax.management.*", "javax.net.ssl.*"})
 @PrepareForTest(HttpUtil.class)
-@SpringBootTest
+@SpringBootTest(
+    classes = {
+        PulsarManagerApplication.class,
+        SqliteDBTestProfile.class
+    }
+)
+@ActiveProfiles("test")
 public class NamespacesServiceImplTest {
 
     @Autowired
diff --git a/src/test/java/com/manager/pulsar/service/TenantsServiceImplTest.java b/src/test/java/com/manager/pulsar/service/TenantsServiceImplTest.java
index 8f066a4..0e534ac 100644
--- a/src/test/java/com/manager/pulsar/service/TenantsServiceImplTest.java
+++ b/src/test/java/com/manager/pulsar/service/TenantsServiceImplTest.java
@@ -14,6 +14,8 @@
 package com.manager.pulsar.service;
 
 import com.google.common.collect.Maps;
+import com.manager.pulsar.PulsarManagerApplication;
+import com.manager.pulsar.profiles.SqliteDBTestProfile;
 import com.manager.pulsar.utils.HttpUtil;
 import org.junit.Assert;
 import org.junit.Test;
@@ -25,6 +27,7 @@
 import org.powermock.modules.junit4.PowerMockRunnerDelegate;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.context.junit4.SpringRunner;
 
 import java.util.ArrayList;
@@ -36,7 +39,13 @@
 @PowerMockRunnerDelegate(SpringRunner.class)
 @PowerMockIgnore( {"javax.management.*", "javax.net.ssl.*"})
 @PrepareForTest(HttpUtil.class)
-@SpringBootTest
+@SpringBootTest(
+    classes = {
+        PulsarManagerApplication.class,
+        SqliteDBTestProfile.class
+    }
+)
+@ActiveProfiles("test")
 public class TenantsServiceImplTest {
 
     @Autowired
diff --git a/src/test/java/com/manager/pulsar/service/TopicsServiceImplTest.java b/src/test/java/com/manager/pulsar/service/TopicsServiceImplTest.java
index dfe7fd0..8337d95 100644
--- a/src/test/java/com/manager/pulsar/service/TopicsServiceImplTest.java
+++ b/src/test/java/com/manager/pulsar/service/TopicsServiceImplTest.java
@@ -14,6 +14,8 @@
 package com.manager.pulsar.service;
 
 import com.google.common.collect.Maps;
+import com.manager.pulsar.PulsarManagerApplication;
+import com.manager.pulsar.profiles.SqliteDBTestProfile;
 import com.manager.pulsar.utils.HttpUtil;
 import org.junit.Assert;
 import org.junit.Test;
@@ -25,6 +27,7 @@
 import org.powermock.modules.junit4.PowerMockRunnerDelegate;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.context.junit4.SpringRunner;
 
 import java.util.Map;
@@ -33,7 +36,13 @@
 @PowerMockRunnerDelegate(SpringRunner.class)
 @PowerMockIgnore( {"javax.management.*", "javax.net.ssl.*"})
 @PrepareForTest(HttpUtil.class)
-@SpringBootTest
+@SpringBootTest(
+    classes = {
+        PulsarManagerApplication.class,
+        SqliteDBTestProfile.class
+    }
+)
+@ActiveProfiles("test")
 public class TopicsServiceImplTest {
 
     @Autowired