Repository: Unleash/unleash-client-java
Pull request #30 (2016-11-09 17:11:24)

Implement client registration and metrics.
Adds implementation where the client register at startup
and sends metrics to the server regularly.

Needs one more thing:
- An option to disable metrics.

@sveisvei 


Patch:
diff --git a/README.md b/README.md
index c3c4aad0..a0ba48c2 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 # The Unleash Client 4 Java
 
 [![Build Status](https://travis-ci.org/Unleash/unleash-client-java.svg?branch=master)](https://travis-ci.org/Unleash/unleash-client-java)
-[![Coverage Status](https://coveralls.io/repos/github/Unleash/unleash-client-java/badge.svg?branch=master)](https://coveralls.io/github/Unleash/unleash-client-java?branch=master)
+[![Coverage Status](https://coveralls.io/repos/Unleash/unleash-client-java/badge.png?branch=master)](https://coveralls.io/r/Unleash/unleash-client-java?branch=master)
 
 ## Create a new a Unleash instance
 
@@ -11,8 +11,14 @@ Spring or Guice to manage this.
 
 You create a new instance with the following command:
 ```java
-URI unleashServer = URI.create("http://unleash.herokuapp.com/features")
-Unleash unleash = new DefaultUnleash(unleashServer);
+
+UnleashConfig config = UnleashConfig.builder()
+            .appName("java-test")
+            .instanceId("instance x")
+            .unleashAPI("http://unleash.herokuapp.com")
+            .build();
+
+Unleash unleash = new DefaultUnleash(config);
 ```
 
 ## Awesome feature toggle API
diff --git a/src/main/java/no/finn/unleash/DefaultUnleash.java b/src/main/java/no/finn/unleash/DefaultUnleash.java
index 2aa53ddb..d6634742 100644
--- a/src/main/java/no/finn/unleash/DefaultUnleash.java
+++ b/src/main/java/no/finn/unleash/DefaultUnleash.java
@@ -1,8 +1,10 @@
 package no.finn.unleash;
 
-import java.net.URI;
 import java.util.HashMap;
 import java.util.Map;
+
+import no.finn.unleash.metric.UnleashMetricService;
+import no.finn.unleash.metric.UnleashMetricServiceImpl;
 import no.finn.unleash.repository.FeatureToggleRepository;
 import no.finn.unleash.repository.ToggleBackupHandlerFile;
 import no.finn.unleash.repository.HttpToggleFetcher;
@@ -10,24 +12,37 @@ import no.finn.unleash.repository.ToggleRepository;
 import no.finn.unleash.strategy.DefaultStrategy;
 import no.finn.unleash.strategy.Strategy;
 import no.finn.unleash.strategy.UnknownStrategy;
+import no.finn.unleash.util.UnleashConfig;
+import no.finn.unleash.util.UnleashScheduledExecutor;
+import no.finn.unleash.util.UnleashScheduledExecutorImpl;
 
 public final class DefaultUnleash implements Unleash {
     private static final DefaultStrategy DEFAULT_STRATEGY = new DefaultStrategy();
     private static final UnknownStrategy UNKNOWN_STRATEGY = new UnknownStrategy();
+    private static final UnleashScheduledExecutor unleashScheduledExecutor = new UnleashScheduledExecutorImpl();
 
+    private final UnleashMetricService metricService;
     private final ToggleRepository toggleRepository;
     private final Map<String, Strategy> strategyMap;
 
-    public DefaultUnleash(URI unleashServer, Strategy... strategies) {
-        this(new FeatureToggleRepository(
-                        new HttpToggleFetcher(unleashServer),
-                        new ToggleBackupHandlerFile()),
-                strategies);
+
+    private static FeatureToggleRepository defaultToggleRepository(UnleashConfig unleashConfig) {
+        return new FeatureToggleRepository(
+                unleashConfig,
+                unleashScheduledExecutor,
+                new HttpToggleFetcher(unleashConfig),
+                new ToggleBackupHandlerFile());
     }
 
-    public DefaultUnleash(ToggleRepository toggleRepository, Strategy... strategies) {
+    public DefaultUnleash(UnleashConfig unleashConfig, Strategy... strategies) {
+        this(unleashConfig, defaultToggleRepository(unleashConfig), strategies);
+    }
+
+    public DefaultUnleash(UnleashConfig unleashConfig, ToggleRepository toggleRepository, Strategy... strategies) {
         this.toggleRepository = toggleRepository;
         this.strategyMap = buildStrategyMap(strategies);
+        this.metricService = new UnleashMetricServiceImpl(unleashConfig, unleashScheduledExecutor);
+        metricService.register(strategyMap.keySet());
     }
 
     @Override
@@ -37,18 +52,22 @@ public final class DefaultUnleash implements Unleash {
 
     @Override
     public boolean isEnabled(final String toggleName, final boolean defaultSetting) {
+        boolean enabled = false;
         FeatureToggle featureToggle = toggleRepository.getToggle(toggleName);
 
         if (featureToggle == null) {
-            return defaultSetting;
+            enabled = defaultSetting;
         } else if(!featureToggle.isEnabled()) {
-            return false;
+            enabled = false;
+        } else {
+            enabled = featureToggle.getStrategies().stream()
+                    .filter(as -> getStrategy(as.getName()).isEnabled(as.getParameters()))
+                    .findFirst()
+                    .isPresent();
         }
 
-        return featureToggle.getStrategies().stream()
-                .filter(as -> getStrategy(as.getName()).isEnabled(as.getParameters()))
-                .findFirst()
-                .isPresent();
+        metricService.count(toggleName, enabled);
+        return enabled;
     }
 
     private Map<String, Strategy> buildStrategyMap(Strategy[] strategies) {
diff --git a/src/main/java/no/finn/unleash/metric/ClientMetrics.java b/src/main/java/no/finn/unleash/metric/ClientMetrics.java
new file mode 100644
index 00000000..f86765f7
--- /dev/null
+++ b/src/main/java/no/finn/unleash/metric/ClientMetrics.java
@@ -0,0 +1,28 @@
+package no.finn.unleash.metric;
+
+import no.finn.unleash.util.UnleashConfig;
+
+class ClientMetrics {
+
+    private final String appName;
+    private final String instanceId;
+    private final MetricsBucket bucket;
+
+    ClientMetrics(UnleashConfig config, MetricsBucket bucket) {
+        this.appName = config.getAppName();
+        this.instanceId = config.getInstanceId();
+        this.bucket = bucket;
+    }
+
+    public String getAppName() {
+        return appName;
+    }
+
+    public String getInstanceId() {
+        return instanceId;
+    }
+
+    public MetricsBucket getBucket() {
+        return bucket;
+    }
+}
diff --git a/src/main/java/no/finn/unleash/metric/ClientRegistration.java b/src/main/java/no/finn/unleash/metric/ClientRegistration.java
new file mode 100644
index 00000000..5c4400ad
--- /dev/null
+++ b/src/main/java/no/finn/unleash/metric/ClientRegistration.java
@@ -0,0 +1,42 @@
+ package no.finn.unleash.metric;
+
+ import no.finn.unleash.util.UnleashConfig;
+
+ import java.time.LocalDateTime;
+ import java.util.Set;
+
+class ClientRegistration {
+    private final String appName;
+    private final String instanceId;
+    private final Set<String> strategies;
+    private final LocalDateTime started;
+    private final long interval;
+
+    ClientRegistration(UnleashConfig config, LocalDateTime started, Set<String> strategies) {
+        this.appName = config.getAppName();
+        this.instanceId = config.getInstanceId();
+        this.started = started;
+        this.strategies = strategies;
+        this.interval = config.getSendMetricsInterval();
+    }
+
+    public String getAppName() {
+        return appName;
+    }
+
+    public String getInstanceId() {
+        return instanceId;
+    }
+
+    public Set<String> getStrategies() {
+        return strategies;
+    }
+
+    public LocalDateTime getStarted() {
+        return started;
+    }
+
+    public long getInterval() {
+        return interval;
+    }
+}
diff --git a/src/main/java/no/finn/unleash/metric/MetricsBucket.java b/src/main/java/no/finn/unleash/metric/MetricsBucket.java
new file mode 100644
index 00000000..07bb6a3d
--- /dev/null
+++ b/src/main/java/no/finn/unleash/metric/MetricsBucket.java
@@ -0,0 +1,43 @@
+package no.finn.unleash.metric;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.HashMap;
+import java.util.Map;
+
+class MetricsBucket {
+    private final Map<String, ToggleCount> toggles;
+    private final LocalDateTime start;
+    private LocalDateTime stop;
+
+    MetricsBucket() {
+        this.start = LocalDateTime.now(ZoneId.of("UTC"));
+        this.toggles = new HashMap<>();
+    }
+
+    void registerCount(String toggleName, boolean active) {
+        if(toggles.containsKey(toggleName)) {
+            toggles.get(toggleName).register(active);
+        } else {
+            ToggleCount counter = new ToggleCount();
+            counter.register(active);
+            toggles.put(toggleName, counter);
+        }
+    }
+
+    void end() {
+        this.stop = LocalDateTime.now(ZoneId.of("UTC"));
+    }
+
+    public Map<String, ToggleCount> getToggles() {
+        return toggles;
+    }
+
+    public LocalDateTime getStart() {
+        return start;
+    }
+
+    public LocalDateTime getStop() {
+        return stop;
+    }
+}
diff --git a/src/main/java/no/finn/unleash/metric/ToggleCount.java b/src/main/java/no/finn/unleash/metric/ToggleCount.java
new file mode 100644
index 00000000..afa1643d
--- /dev/null
+++ b/src/main/java/no/finn/unleash/metric/ToggleCount.java
@@ -0,0 +1,27 @@
+package no.finn.unleash.metric;
+
+class ToggleCount {
+    private long yes;
+    private long no;
+
+    public ToggleCount() {
+        this.yes = 0;
+        this.no = 0;
+    }
+
+    public void register(boolean active) {
+        if(active) {
+            yes++;
+        } else {
+            no++;
+        }
+    }
+
+    public long getYes() {
+        return yes;
+    }
+
+    public long getNo() {
+        return no;
+    }
+}
diff --git a/src/main/java/no/finn/unleash/metric/UnleashMetricService.java b/src/main/java/no/finn/unleash/metric/UnleashMetricService.java
new file mode 100644
index 00000000..5f6e47eb
--- /dev/null
+++ b/src/main/java/no/finn/unleash/metric/UnleashMetricService.java
@@ -0,0 +1,8 @@
+package no.finn.unleash.metric;
+
+import java.util.Set;
+
+public interface UnleashMetricService {
+    void register(Set<String> strategies);
+    void count(String toggleName, boolean active);
+}
diff --git a/src/main/java/no/finn/unleash/metric/UnleashMetricServiceImpl.java b/src/main/java/no/finn/unleash/metric/UnleashMetricServiceImpl.java
new file mode 100644
index 00000000..ac4e217d
--- /dev/null
+++ b/src/main/java/no/finn/unleash/metric/UnleashMetricServiceImpl.java
@@ -0,0 +1,55 @@
+package no.finn.unleash.metric;
+
+import no.finn.unleash.util.UnleashConfig;
+import no.finn.unleash.util.UnleashScheduledExecutor;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Set;
+
+public class UnleashMetricServiceImpl implements UnleashMetricService {
+    private final LocalDateTime started;
+    private final UnleashConfig unleashConfig;
+    private final long metricsInterval;
+    private final UnleashMetricsSender unleashMetricsSender;
+
+    //mutable
+    private MetricsBucket currentMetricsBucket;
+
+    public UnleashMetricServiceImpl(UnleashConfig unleashConfig, UnleashScheduledExecutor executor) {
+        this(unleashConfig, new UnleashMetricsSender(unleashConfig), executor);
+    }
+
+    public UnleashMetricServiceImpl(UnleashConfig unleashConfig,
+                                    UnleashMetricsSender unleashMetricsSender,
+                                    UnleashScheduledExecutor executor) {
+        this.currentMetricsBucket = new MetricsBucket();
+        this.started = LocalDateTime.now(ZoneId.of("UTC"));
+        this.unleashConfig = unleashConfig;
+        this.metricsInterval = unleashConfig.getSendMetricsInterval();
+        this.unleashMetricsSender = unleashMetricsSender;
+
+        executor.setInterval(sendMetrics(), metricsInterval, metricsInterval);
+    }
+
+    @Override
+    public void register(Set<String> strategies) {
+        ClientRegistration registration = new ClientRegistration(unleashConfig, started, strategies);
+        unleashMetricsSender.registerClient(registration);
+    }
+
+    @Override
+    public void count(String toggleName, boolean active) {
+        currentMetricsBucket.registerCount(toggleName, active);
+    }
+
+    private Runnable sendMetrics() {
+        return () -> {
+            MetricsBucket metricsBucket = this.currentMetricsBucket;
+            this.currentMetricsBucket = new MetricsBucket();
+            metricsBucket.end();
+            ClientMetrics metrics = new ClientMetrics(unleashConfig, metricsBucket);
+            unleashMetricsSender.sendMetrics(metrics);
+        };
+    }
+}
diff --git a/src/main/java/no/finn/unleash/metric/UnleashMetricsSender.java b/src/main/java/no/finn/unleash/metric/UnleashMetricsSender.java
new file mode 100644
index 00000000..209adaa3
--- /dev/null
+++ b/src/main/java/no/finn/unleash/metric/UnleashMetricsSender.java
@@ -0,0 +1,102 @@
+package no.finn.unleash.metric;
+
+import com.google.gson.*;
+import no.finn.unleash.UnleashException;
+import no.finn.unleash.util.UnleashConfig;
+import no.finn.unleash.util.UnleashURLs;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.lang.reflect.Type;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+
+import static java.time.format.DateTimeFormatter.ISO_INSTANT;
+
+public class UnleashMetricsSender {
+    private static final Logger LOG = LogManager.getLogger();
+    private static final int CONNECT_TIMEOUT = 1000;
+
+    private final Gson gson;
+    private UnleashConfig unleashConfig;
+    private final URL clientRegistrationURL;
+    private final URL clientMetricsURL;
+
+    public UnleashMetricsSender(UnleashConfig unleashConfig) {
+        this.unleashConfig = unleashConfig;
+        UnleashURLs urls =  unleashConfig.getUnleashURLs();
+        this.clientMetricsURL = urls.getClientMetricsURL();
+        this.clientRegistrationURL = urls.getClientRegisterURL();
+
+        this.gson = new GsonBuilder()
+                .registerTypeAdapter(LocalDateTime.class, new DateTimeSerializer())
+                .create();
+    }
+
+    static class DateTimeSerializer implements JsonSerializer<LocalDateTime> {
+        @Override
+        public JsonElement serialize(
+                LocalDateTime localDateTime, Type type, JsonSerializationContext jsonSerializationContext) {
+            return new JsonPrimitive(ISO_INSTANT.format(localDateTime.toInstant(ZoneOffset.UTC)));
+        };
+    }
+
+    public void registerClient(ClientRegistration registration) {
+        if(!unleashConfig.isDisableMetrics()) {
+            try {
+                post(clientRegistrationURL, registration);
+            } catch(UnleashException ex) {
+                LOG.warn("failed to register client", ex);
+            }
+        }
+    }
+
+    public void sendMetrics(ClientMetrics metrics) {
+        if(!unleashConfig.isDisableMetrics()) {
+            try {
+                post(clientMetricsURL, metrics);
+            } catch(UnleashException ex) {
+                LOG.warn("failed to send metrics", ex);
+            }
+        }
+    }
+
+    private int post(URL url, Object o) throws UnleashException {
+
+        HttpURLConnection connection = null;
+        try {
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setConnectTimeout(CONNECT_TIMEOUT);
+            connection.setReadTimeout(CONNECT_TIMEOUT);
+            connection.setRequestMethod("POST");
+            connection.setRequestProperty("Accept", "application/json");
+            connection.setRequestProperty("Content-Type", "application/json");
+            connection.setRequestProperty("UNLEASH-APPNAME", this.unleashConfig.getAppName());
+            connection.setRequestProperty("UNLEASH-INSTANCEID", this.unleashConfig.getInstanceId());
+            connection.setUseCaches (false);
+            connection.setDoInput(true);
+            connection.setDoOutput(true);
+
+            OutputStreamWriter wr = new OutputStreamWriter(connection.getOutputStream());
+            gson.toJson(o, wr);
+            wr.flush();
+            wr.close();
+
+            connection.connect();
+
+            return connection.getResponseCode();
+        } catch (IOException e) {
+            throw new UnleashException("Could not post to Unleash API", e);
+        } catch (IllegalStateException e) {
+            throw new UnleashException(e.getMessage(), e);
+        } finally {
+            if(connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+}
diff --git a/src/main/java/no/finn/unleash/repository/FeatureToggleRepository.java b/src/main/java/no/finn/unleash/repository/FeatureToggleRepository.java
index 1d571b22..117858f3 100644
--- a/src/main/java/no/finn/unleash/repository/FeatureToggleRepository.java
+++ b/src/main/java/no/finn/unleash/repository/FeatureToggleRepository.java
@@ -1,5 +1,8 @@
 package no.finn.unleash.repository;
 
+import no.finn.unleash.util.UnleashConfig;
+import no.finn.unleash.util.UnleashScheduledExecutor;
+import no.finn.unleash.util.UnleashScheduledExecutorImpl;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
@@ -10,53 +13,37 @@ import no.finn.unleash.UnleashException;
 public final class FeatureToggleRepository implements ToggleRepository {
     private static final Logger LOG = LogManager.getLogger();
 
-    private static final ScheduledThreadPoolExecutor TIMER = new ScheduledThreadPoolExecutor(
-            1,
-            r -> {
-                Thread thread = Executors.defaultThreadFactory().newThread(r);
-                thread.setName("unleash-toggle-repository");
-                thread.setDaemon(true);
-                return thread;
-            });
-
-    static {
-        TIMER.setRemoveOnCancelPolicy(true);
-    }
-
     private final ToggleBackupHandler toggleBackupHandler;
     private final ToggleFetcher toggleFetcher;
 
     private ToggleCollection toggleCollection;
 
-    public FeatureToggleRepository(ToggleFetcher toggleFetcher, ToggleBackupHandler toggleBackupHandler) {
-        this(toggleFetcher, toggleBackupHandler, 10L);
-    }
+    public FeatureToggleRepository(
+            UnleashConfig unleashConfig,
+            UnleashScheduledExecutor executor,
+            ToggleFetcher toggleFetcher,
+            ToggleBackupHandler toggleBackupHandler) {
 
-    public FeatureToggleRepository(ToggleFetcher toggleFetcher, ToggleBackupHandler toggleBackupHandler, long pollIntervalSeconds) {
         this.toggleBackupHandler = toggleBackupHandler;
         this.toggleFetcher = toggleFetcher;
 
         toggleCollection = toggleBackupHandler.read();
-        startBackgroundPolling(pollIntervalSeconds);
+
+        executor.setInterval(updateToggles(), 0, unleashConfig.getFetchTogglesInterval());
     }
 
-    private ScheduledFuture startBackgroundPolling(long pollIntervalSeconds) {
-        try {
-            return TIMER.scheduleAtFixedRate(() -> {
-                try {
-                    Response response = toggleFetcher.fetchToggles();
-                    if (response.getStatus() == Response.Status.CHANGED) {
-                        toggleCollection = response.getToggleCollection();
-                        toggleBackupHandler.write(response.getToggleCollection());
-                    }
-                } catch (UnleashException e) {
-                    LOG.warn("Could not refresh feature toggles", e);
+    private Runnable updateToggles() {
+        return () -> {
+            try {
+                FeatureToggleResponse response = toggleFetcher.fetchToggles();
+                if (response.getStatus() == FeatureToggleResponse.Status.CHANGED) {
+                    toggleCollection = response.getToggleCollection();
+                    toggleBackupHandler.write(response.getToggleCollection());
                 }
-            }, 0, pollIntervalSeconds, TimeUnit.SECONDS);
-        } catch (RejectedExecutionException ex) {
-            LOG.error("Unleash background task crashed", ex);
-            return null;
-        }
+            } catch (UnleashException e) {
+                LOG.warn("Could not refresh feature toggles", e);
+            }
+        };
     }
 
     @Override
diff --git a/src/main/java/no/finn/unleash/repository/Response.java b/src/main/java/no/finn/unleash/repository/FeatureToggleResponse.java
similarity index 78%
rename from src/main/java/no/finn/unleash/repository/Response.java
rename to src/main/java/no/finn/unleash/repository/FeatureToggleResponse.java
index 0366a080..cc639b14 100644
--- a/src/main/java/no/finn/unleash/repository/Response.java
+++ b/src/main/java/no/finn/unleash/repository/FeatureToggleResponse.java
@@ -5,18 +5,18 @@ import no.finn.unleash.FeatureToggle;
 import java.util.Collections;
 import java.util.List;
 
-final class Response {
+public final class FeatureToggleResponse {
     enum Status {NOT_CHANGED, CHANGED}
 
     private final Status status;
     private final ToggleCollection toggleCollection;
 
-    public Response(Status status, ToggleCollection toggleCollection) {
+    public FeatureToggleResponse(Status status, ToggleCollection toggleCollection) {
         this.status = status;
         this.toggleCollection = toggleCollection;
     }
 
-    public Response(Status status) {
+    public FeatureToggleResponse(Status status) {
         this.status = status;
         List<FeatureToggle> emptyList = Collections.emptyList();
         this.toggleCollection = new ToggleCollection(emptyList);
diff --git a/src/main/java/no/finn/unleash/repository/HttpToggleFetcher.java b/src/main/java/no/finn/unleash/repository/HttpToggleFetcher.java
index 82443657..dd397c0e 100644
--- a/src/main/java/no/finn/unleash/repository/HttpToggleFetcher.java
+++ b/src/main/java/no/finn/unleash/repository/HttpToggleFetcher.java
@@ -5,28 +5,25 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.URI;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
 import no.finn.unleash.UnleashException;
+import no.finn.unleash.util.UnleashConfig;
 
 public final class HttpToggleFetcher implements ToggleFetcher {
     public static final int CONNECT_TIMEOUT = 10000;
     private String etag = "";
 
     private final URL toggleUrl;
+    private UnleashConfig unleashConfig;
 
-    public HttpToggleFetcher(URI repo) {
-        try {
-            toggleUrl = repo.toURL();
-        } catch (MalformedURLException|IllegalArgumentException ex) {
-            throw new UnleashException("Invalid unleash repository uri [" + repo.toString() + "]", ex);
-        }
+    public HttpToggleFetcher(UnleashConfig unleashConfig) {
+        this.unleashConfig = unleashConfig;
+        this.toggleUrl = unleashConfig.getUnleashURLs().getFetchTogglesURL();
     }
 
     @Override
-    public Response fetchToggles() throws UnleashException {
+    public FeatureToggleResponse fetchToggles() throws UnleashException {
         HttpURLConnection connection = null;
         try {
             connection = (HttpURLConnection) toggleUrl.openConnection();
@@ -34,6 +31,9 @@ public final class HttpToggleFetcher implements ToggleFetcher {
             connection.setReadTimeout(CONNECT_TIMEOUT);
             connection.setRequestProperty("Accept", "application/json");
             connection.setRequestProperty("Content-Type", "application/json");
+            connection.setRequestProperty("UNLEASH-APPNAME", this.unleashConfig.getAppName());
+            connection.setRequestProperty("UNLEASH-INSTANCEID", this.unleashConfig.getInstanceId());
+
             connection.setRequestProperty("If-None-Match", etag);
             connection.setUseCaches(true);
             connection.connect();
@@ -42,7 +42,7 @@ public final class HttpToggleFetcher implements ToggleFetcher {
             if(responseCode < 300) {
                 return getToggleResponse(connection);
             } else {
-                return new Response(Response.Status.NOT_CHANGED);
+                return new FeatureToggleResponse(FeatureToggleResponse.Status.NOT_CHANGED);
             }
         } catch (IOException e) {
             throw new UnleashException("Could not fetch toggles", e);
@@ -55,14 +55,14 @@ public final class HttpToggleFetcher implements ToggleFetcher {
         }
     }
 
-    private Response getToggleResponse(HttpURLConnection request) throws IOException {
+    private FeatureToggleResponse getToggleResponse(HttpURLConnection request) throws IOException {
         etag = request.getHeaderField("ETag");
 
         try(BufferedReader reader = new BufferedReader(
                 new InputStreamReader((InputStream) request.getContent(), StandardCharsets.UTF_8))) {
 
             ToggleCollection toggles = JsonToggleParser.fromJson(reader);
-            return new Response(Response.Status.CHANGED, toggles);
+            return new FeatureToggleResponse(FeatureToggleResponse.Status.CHANGED, toggles);
         }
     }
 }
diff --git a/src/main/java/no/finn/unleash/repository/ToggleFetcher.java b/src/main/java/no/finn/unleash/repository/ToggleFetcher.java
index ec98d44a..b41c9707 100644
--- a/src/main/java/no/finn/unleash/repository/ToggleFetcher.java
+++ b/src/main/java/no/finn/unleash/repository/ToggleFetcher.java
@@ -3,5 +3,5 @@ package no.finn.unleash.repository;
 import no.finn.unleash.UnleashException;
 
 public interface ToggleFetcher {
-   Response fetchToggles() throws UnleashException;
+   FeatureToggleResponse fetchToggles() throws UnleashException;
 }
diff --git a/src/main/java/no/finn/unleash/util/UnleashConfig.java b/src/main/java/no/finn/unleash/util/UnleashConfig.java
new file mode 100644
index 00000000..ac4071c8
--- /dev/null
+++ b/src/main/java/no/finn/unleash/util/UnleashConfig.java
@@ -0,0 +1,126 @@
+package no.finn.unleash.util;
+
+import java.net.URI;
+
+public class UnleashConfig {
+    private final URI unleashAPI;
+    private final UnleashURLs unleashURLs;
+    private final String appName;
+    private final String instanceId;
+    private final long fetchTogglesInterval;
+    private final long sendMetricsInterval;
+    private final boolean disableMetrics;
+
+    public UnleashConfig(
+            URI unleashAPI,
+            String appName,
+            String instanceId,
+            long fetchTogglesInterval,
+            long sendMetricsInterval,
+            boolean disableMetrics) {
+
+        if(appName == null) {
+            throw new IllegalStateException("You are required to specify the unleash appName");
+        }
+
+        if(unleashAPI == null) {
+            throw new IllegalStateException("You are required to specify the unleashAPI url");
+        }
+
+        this.unleashAPI = unleashAPI;
+        this.unleashURLs = new UnleashURLs(unleashAPI);
+        this.appName = appName;
+        this.instanceId = instanceId;
+        this.fetchTogglesInterval = fetchTogglesInterval;
+        this.sendMetricsInterval = sendMetricsInterval;
+        this.disableMetrics = disableMetrics;
+    }
+
+    public URI getUnleashAPI() {
+        return unleashAPI;
+    }
+
+    public String getAppName() {
+        return appName;
+    }
+
+    public String getInstanceId() {
+        return instanceId;
+    }
+
+    public long getFetchTogglesInterval() {
+        return fetchTogglesInterval;
+    }
+
+    public long getSendMetricsInterval() {
+        return sendMetricsInterval;
+    }
+
+    public UnleashURLs getUnleashURLs() {
+        return unleashURLs;
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public boolean isDisableMetrics() {
+        return disableMetrics;
+    }
+
+    public static class Builder {
+        private URI unleashAPI;
+        private String appName;
+        private String instanceId = "generated-"+Math.round(Math.random() * 1000000);
+        private long fetchTogglesInterval = 10;
+        private long sendMetricsInterval = 60;
+        private boolean disableMetrics = false;
+
+
+        public Builder unleashAPI(URI unleashAPI) {
+            this.unleashAPI = unleashAPI;
+            return this;
+        }
+
+        public Builder unleashAPI(String unleashAPI) {
+            this.unleashAPI = URI.create(unleashAPI);
+            return this;
+        }
+
+        public Builder appName(String appName) {
+            this.appName = appName;
+            return this;
+        }
+
+        public Builder instanceId(String instanceId) {
+            this.instanceId = instanceId;
+            return this;
+        }
+
+        public Builder fetchTogglesInterval(long fetchTogglesInterval) {
+            this.fetchTogglesInterval = fetchTogglesInterval;
+            return this;
+        }
+
+        public Builder sendMetricsInterval(long sendMetricsInterval) {
+            this.sendMetricsInterval = sendMetricsInterval;
+            return this;
+        }
+
+        public Builder disableMetrics() {
+            this.disableMetrics = true;
+            return this;
+        }
+
+
+        public UnleashConfig build() {
+            return new UnleashConfig(
+                    unleashAPI,
+                    appName,
+                    instanceId,
+                    fetchTogglesInterval,
+                    sendMetricsInterval,
+                    disableMetrics);
+        }
+    }
+}
diff --git a/src/main/java/no/finn/unleash/util/UnleashScheduledExecutor.java b/src/main/java/no/finn/unleash/util/UnleashScheduledExecutor.java
new file mode 100644
index 00000000..b1126570
--- /dev/null
+++ b/src/main/java/no/finn/unleash/util/UnleashScheduledExecutor.java
@@ -0,0 +1,8 @@
+package no.finn.unleash.util;
+
+import java.util.concurrent.*;
+
+public interface UnleashScheduledExecutor {
+       ScheduledFuture setInterval(
+               Runnable command, long initialDelaySec, long periodSec) throws RejectedExecutionException;
+}
diff --git a/src/main/java/no/finn/unleash/util/UnleashScheduledExecutorImpl.java b/src/main/java/no/finn/unleash/util/UnleashScheduledExecutorImpl.java
new file mode 100644
index 00000000..ae7b2b86
--- /dev/null
+++ b/src/main/java/no/finn/unleash/util/UnleashScheduledExecutorImpl.java
@@ -0,0 +1,37 @@
+package no.finn.unleash.util;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.concurrent.*;
+
+public class UnleashScheduledExecutorImpl implements UnleashScheduledExecutor {
+    private static final Logger LOG = LogManager.getLogger();
+
+    private final ScheduledThreadPoolExecutor timer;
+
+    public UnleashScheduledExecutorImpl() {
+        this.timer = new ScheduledThreadPoolExecutor(
+                1,
+                r -> {
+                    Thread thread = Executors.defaultThreadFactory().newThread(r);
+                    thread.setName("unleash-api-executor");
+                    thread.setDaemon(true);
+                    return thread;
+                });
+        this.timer.setRemoveOnCancelPolicy(true);
+    }
+
+    @Override
+    public ScheduledFuture setInterval(Runnable command,
+                                                      long initialDelaySec,
+                                                      long periodSec) {
+        try {
+            return timer.scheduleAtFixedRate(command, initialDelaySec, periodSec, TimeUnit.SECONDS);
+        } catch (RejectedExecutionException ex) {
+            LOG.error("Unleash background task crashed", ex);
+            return null;
+        }
+
+    }
+}
diff --git a/src/main/java/no/finn/unleash/util/UnleashURLs.java b/src/main/java/no/finn/unleash/util/UnleashURLs.java
new file mode 100644
index 00000000..4f316971
--- /dev/null
+++ b/src/main/java/no/finn/unleash/util/UnleashURLs.java
@@ -0,0 +1,35 @@
+package no.finn.unleash.util;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+
+public class UnleashURLs {
+    private final URL fetchTogglesURL;
+    private final URL clientMetricsURL;
+    private final URL clientRegisterURL;
+
+    public UnleashURLs(URI unleashAPI) {
+        try {
+            String unleashAPIstr = unleashAPI.toString();
+            fetchTogglesURL = URI.create(unleashAPIstr + "/features").normalize().toURL();
+            clientMetricsURL = URI.create(unleashAPIstr + "/client/metrics").normalize().toURL();
+            clientRegisterURL = URI.create(unleashAPIstr + "/client/register").normalize().toURL();
+
+        } catch (MalformedURLException ex) {
+            throw new IllegalArgumentException("Unleash API is not a valid URL: " + unleashAPI);
+        }
+    }
+
+    public URL getFetchTogglesURL() {
+        return fetchTogglesURL;
+    }
+
+    public URL getClientMetricsURL() {
+        return clientMetricsURL;
+    }
+
+    public URL getClientRegisterURL() {
+        return clientRegisterURL;
+    }
+}


Test patch:
diff --git a/src/test/java/no/finn/unleash/ManualTesting.java b/src/test/java/no/finn/unleash/ManualTesting.java
index d2854ed1..e332a2b3 100644
--- a/src/test/java/no/finn/unleash/ManualTesting.java
+++ b/src/test/java/no/finn/unleash/ManualTesting.java
@@ -1,16 +1,34 @@
 package no.finn.unleash;
 
-import java.net.URI;
+import java.util.Map;
 import java.util.Random;
 
-import no.finn.unleash.repository.*;
+import no.finn.unleash.strategy.Strategy;
+import no.finn.unleash.util.UnleashConfig;
 
 public class ManualTesting {
     public static void main(String[] args) throws Exception {
-        ToggleFetcher toggleFetcher = new HttpToggleFetcher(URI.create("http://localhost:4242/features"));
-        ToggleBackupHandler toggleBackupHandler = new ToggleBackupHandlerFile();
-        ToggleRepository repository = new FeatureToggleRepository(toggleFetcher, toggleBackupHandler, 1);
-        Unleash unleash = new DefaultUnleash(repository);
+        Strategy strategy = new Strategy() {
+            @Override
+            public String getName() {
+                return "ActiveForUserWithId";
+            }
+
+            @Override
+            public boolean isEnabled(Map<String, String> parameters) {
+                System.out.println("parameters = " + parameters);
+                return true;
+            }
+        };
+        UnleashConfig unleashConfig = new UnleashConfig.Builder()
+                .appName("java-test")
+                .instanceId("instance y")
+                .unleashAPI("https://unleash-new-ui.herokuapp.com/api/")
+                .fetchTogglesInterval(1)
+                .sendMetricsInterval(10)
+                .build();
+
+        Unleash unleash = new DefaultUnleash(unleashConfig, strategy);
 
         for(int i=0;i<100;i++) {
             (new Thread(new UnleashThread(unleash, "thread-"+i, 100))).start();
@@ -34,10 +52,10 @@ public class ManualTesting {
             while(currentRound < maxRounds) {
                 currentRound++;
                 long startTime = System.nanoTime();
-                boolean enabled = unleash.isEnabled("featureX");
+                boolean enabled = unleash.isEnabled("Demo");
                 long timeUsed = System.nanoTime() - startTime;
 
-                System.out.println(name + "\t" +"featureX" +":"  + enabled + "\t " + timeUsed + "ns");
+                System.out.println(name + "\t" +"Demo" +":"  + enabled + "\t " + timeUsed + "ns");
 
                 try {
                     //Wait 1 to 10ms before next round
diff --git a/src/test/java/no/finn/unleash/UnleashTest.java b/src/test/java/no/finn/unleash/UnleashTest.java
index a801b740..a801e93c 100644
--- a/src/test/java/no/finn/unleash/UnleashTest.java
+++ b/src/test/java/no/finn/unleash/UnleashTest.java
@@ -2,9 +2,11 @@ package no.finn.unleash;
 
 import no.finn.unleash.repository.ToggleRepository;
 import no.finn.unleash.strategy.Strategy;
+import no.finn.unleash.util.UnleashConfig;
 import org.junit.Before;
 import org.junit.Test;
 
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.Arrays;
 
@@ -20,7 +22,11 @@ public class UnleashTest {
     @Before
     public void setup() {
         toggleRepository = mock(ToggleRepository.class);
-        unleash = new DefaultUnleash(toggleRepository);
+        UnleashConfig config = new UnleashConfig.Builder()
+                .appName("test")
+                .unleashAPI("http://localhost:4242")
+                .build();
+        unleash = new DefaultUnleash(config, toggleRepository);
     }
 
     @Test
@@ -58,7 +64,11 @@ public class UnleashTest {
         when(customStrategy.getName()).thenReturn("custom");
 
         //register custom strategy
-        unleash = new DefaultUnleash(toggleRepository, customStrategy);
+        UnleashConfig config = new UnleashConfig.Builder()
+                .appName("test")
+                .unleashAPI("http://localhost:4242")
+                .build();
+        unleash = new DefaultUnleash(config, toggleRepository, customStrategy);
         when(toggleRepository.getToggle("test")).thenReturn(new FeatureToggle("test", true, Arrays.asList(new ActivationStrategy("custom", null))));
 
         unleash.isEnabled("test");
diff --git a/src/test/java/no/finn/unleash/example/UnleashUsageTest.java b/src/test/java/no/finn/unleash/example/UnleashUsageTest.java
index e6f4d855..c79efd98 100644
--- a/src/test/java/no/finn/unleash/example/UnleashUsageTest.java
+++ b/src/test/java/no/finn/unleash/example/UnleashUsageTest.java
@@ -1,20 +1,23 @@
 package no.finn.unleash.example;
 
-import no.finn.unleash.Unleash;
-import no.finn.unleash.repository.FeatureToggleRepository;
-import no.finn.unleash.repository.ToggleRepository;
 import no.finn.unleash.DefaultUnleash;
+import no.finn.unleash.Unleash;
+import no.finn.unleash.util.UnleashConfig;
 import org.junit.Test;
 
-import java.net.URI;
-
 import static org.junit.Assert.assertFalse;
 
 public class UnleashUsageTest {
 
     @Test
     public void wire() {
-        Unleash unleash = new DefaultUnleash(URI.create("http://localhost:4242/features"), new CustomStrategy());
+        UnleashConfig config = new UnleashConfig.Builder()
+                .appName("test")
+                .instanceId("my-hostname:6517")
+                .unleashAPI("http://localhost:4242")
+                .build();
+
+        Unleash unleash = new DefaultUnleash(config, new CustomStrategy());
 
         assertFalse(unleash.isEnabled("myFeature"));
     }
diff --git a/src/test/java/no/finn/unleash/metric/UnleashMetricServiceImplTest.java b/src/test/java/no/finn/unleash/metric/UnleashMetricServiceImplTest.java
new file mode 100644
index 00000000..44745e0c
--- /dev/null
+++ b/src/test/java/no/finn/unleash/metric/UnleashMetricServiceImplTest.java
@@ -0,0 +1,131 @@
+package no.finn.unleash.metric;
+
+import no.finn.unleash.util.UnleashConfig;
+import no.finn.unleash.util.UnleashScheduledExecutor;
+import org.hamcrest.core.IsNull;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class UnleashMetricServiceImplTest {
+
+    @Test
+    public void should_register_future_for_sending_interval_regualry() {
+        long interval = 10;
+        UnleashConfig config = UnleashConfig
+                .builder()
+                .appName("test")
+                .sendMetricsInterval(interval)
+                .unleashAPI("http://unleash.com")
+                .build();
+        UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
+        UnleashMetricService unleashMetricService = new UnleashMetricServiceImpl(config, executor);
+
+        verify(executor, times(1)).setInterval(any(Runnable.class), eq(interval), eq(interval));
+    }
+
+    @Test
+    public void should_register_client() {
+        long interval = 10;
+        UnleashConfig config = UnleashConfig
+                .builder()
+                .appName("test")
+                .sendMetricsInterval(interval)
+                .unleashAPI("http://unleash.com")
+                .build();
+
+        UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
+        UnleashMetricsSender sender = mock(UnleashMetricsSender.class);
+
+        UnleashMetricService unleashMetricService = new UnleashMetricServiceImpl(config, sender, executor);
+        Set<String> strategies = new HashSet<>();
+        strategies.add("default");
+        strategies.add("custom");
+        unleashMetricService.register(strategies);
+
+        ArgumentCaptor<ClientRegistration> argument = ArgumentCaptor.forClass(ClientRegistration.class);
+
+        verify(sender).registerClient(argument.capture());
+        assertThat(argument.getValue().getAppName(), is(config.getAppName()));
+        assertThat(argument.getValue().getInstanceId(), is(config.getInstanceId()));
+        assertThat(argument.getValue().getStarted(), is(not(IsNull.nullValue())));
+        assertThat(argument.getValue().getStrategies().size(), is(2));
+        assertTrue(argument.getValue().getStrategies().contains("default"));
+        assertTrue(argument.getValue().getStrategies().contains("custom"));
+    }
+
+
+    @Test
+    public void should_send_metrics() {
+        UnleashConfig config = UnleashConfig
+                .builder()
+                .appName("test")
+                .sendMetricsInterval(10)
+                .unleashAPI("http://unleash.com")
+                .build();
+
+        UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
+        UnleashMetricsSender sender = mock(UnleashMetricsSender.class);
+
+        UnleashMetricService unleashMetricService = new UnleashMetricServiceImpl(config, sender, executor);
+
+        ArgumentCaptor<Runnable> sendMetricsCallback = ArgumentCaptor.forClass(Runnable.class);
+        verify(executor).setInterval(sendMetricsCallback.capture(), anyLong(), anyLong());
+
+
+        sendMetricsCallback.getValue().run();
+        verify(sender, times(1)).sendMetrics(any(ClientMetrics.class));
+    }
+
+    @Test
+    public void should_record_and_send_metrics() {
+        UnleashConfig config = UnleashConfig
+                .builder()
+                .appName("test")
+                .sendMetricsInterval(10)
+                .unleashAPI("http://unleash.com")
+                .build();
+
+        UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
+        UnleashMetricsSender sender = mock(UnleashMetricsSender.class);
+
+        UnleashMetricService unleashMetricService = new UnleashMetricServiceImpl(config, sender, executor);
+        unleashMetricService.count("someToggle", true);
+        unleashMetricService.count("someToggle", false);
+        unleashMetricService.count("someToggle", true);
+        unleashMetricService.count("otherToggle", true);
+
+        //Call the sendMetricsCallback
+        ArgumentCaptor<Runnable> sendMetricsCallback = ArgumentCaptor.forClass(Runnable.class);
+        verify(executor).setInterval(sendMetricsCallback.capture(), anyLong(), anyLong());
+        sendMetricsCallback.getValue().run();
+
+        ArgumentCaptor<ClientMetrics> clientMetricsArgumentCaptor = ArgumentCaptor.forClass(ClientMetrics.class);
+        verify(sender).sendMetrics(clientMetricsArgumentCaptor.capture());
+
+
+        ClientMetrics clientMetrics = clientMetricsArgumentCaptor.getValue();
+        MetricsBucket bucket = clientMetricsArgumentCaptor.getValue().getBucket();
+
+        assertThat(clientMetrics.getAppName(), is(config.getAppName()));
+        assertThat(clientMetrics.getInstanceId(), is(config.getInstanceId()));
+        assertNotNull(bucket.getStart());
+        assertNotNull(bucket.getStop());
+        assertThat(bucket.getToggles().size(), is(2));
+        assertThat(bucket.getToggles().get("someToggle").getYes(), is(2l));
+        assertThat(bucket.getToggles().get("someToggle").getNo(), is(1l));
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/java/no/finn/unleash/metric/UnleashMetricsSenderTest.java b/src/test/java/no/finn/unleash/metric/UnleashMetricsSenderTest.java
new file mode 100644
index 00000000..3682e6d4
--- /dev/null
+++ b/src/test/java/no/finn/unleash/metric/UnleashMetricsSenderTest.java
@@ -0,0 +1,82 @@
+package no.finn.unleash.metric;
+
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import no.finn.unleash.util.UnleashConfig;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.LocalDateTime;
+import java.util.HashSet;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+
+public class UnleashMetricsSenderTest {
+    @Rule
+    public WireMockRule wireMockRule = new WireMockRule(0);
+
+    @Rule
+    public ExpectedException exception = ExpectedException.none();
+
+    @Test
+    public void should_send_client_registration() throws URISyntaxException {
+        stubFor(post(urlEqualTo("/client/register"))
+                .withHeader("UNLEASH-APPNAME", matching("test-app"))
+                .willReturn(aResponse().withStatus(200)));
+
+        URI uri = new URI("http://localhost:"+wireMockRule.port());
+        UnleashConfig config = UnleashConfig.builder().appName("test-app").unleashAPI(uri).build();
+
+        UnleashMetricsSender sender = new UnleashMetricsSender(config);
+        sender.registerClient(new ClientRegistration(config, LocalDateTime.now(), new HashSet<String>()));
+
+        verify(postRequestedFor(urlMatching("/client/register"))
+                .withRequestBody(matching(".*appName.*"))
+                .withRequestBody(matching(".*strategies.*"))
+                .withHeader("UNLEASH-APPNAME", matching("test-app")));
+    }
+
+    @Test
+    public void should_send_client_metrics() throws URISyntaxException {
+        stubFor(post(urlEqualTo("/client/metrics"))
+                .withHeader("UNLEASH-APPNAME", matching("test-app"))
+                .willReturn(aResponse().withStatus(200)));
+
+        URI uri = new URI("http://localhost:"+wireMockRule.port());
+        UnleashConfig config = UnleashConfig.builder().appName("test-app").unleashAPI(uri).build();
+
+        UnleashMetricsSender sender = new UnleashMetricsSender(config);
+        MetricsBucket bucket = new MetricsBucket();
+        ClientMetrics metrics = new ClientMetrics(config, bucket);
+        sender.sendMetrics(metrics);
+
+        verify(postRequestedFor(urlMatching("/client/metrics"))
+                .withRequestBody(matching(".*appName.*"))
+                .withRequestBody(matching(".*bucket.*"))
+                .withHeader("UNLEASH-APPNAME", matching("test-app")));
+    }
+
+    @Test
+    public void should_handle_service_failure_when_sending_metrics() throws URISyntaxException {
+        stubFor(post(urlEqualTo("/client/metrics"))
+                .withHeader("UNLEASH-APPNAME", matching("test-app"))
+                .willReturn(aResponse().withStatus(500)));
+
+        URI uri = new URI("http://localhost:"+wireMockRule.port());
+        UnleashConfig config = UnleashConfig.builder().appName("test-app").unleashAPI(uri).build();
+
+        UnleashMetricsSender sender = new UnleashMetricsSender(config);
+        MetricsBucket bucket = new MetricsBucket();
+        ClientMetrics metrics = new ClientMetrics(config, bucket);
+        sender.sendMetrics(metrics);
+
+        verify(postRequestedFor(urlMatching("/client/metrics"))
+                .withRequestBody(matching(".*appName.*"))
+                .withRequestBody(matching(".*bucket.*"))
+                .withHeader("UNLEASH-APPNAME", matching("test-app")));
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/java/no/finn/unleash/repository/FeatureToggleRepositoryTest.java b/src/test/java/no/finn/unleash/repository/FeatureToggleRepositoryTest.java
index d9917be7..cb91f715 100644
--- a/src/test/java/no/finn/unleash/repository/FeatureToggleRepositoryTest.java
+++ b/src/test/java/no/finn/unleash/repository/FeatureToggleRepositoryTest.java
@@ -2,9 +2,11 @@ package no.finn.unleash.repository;
 
 import no.finn.unleash.ActivationStrategy;
 import no.finn.unleash.FeatureToggle;
+import no.finn.unleash.util.UnleashConfig;
+import no.finn.unleash.util.UnleashScheduledExecutor;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 
-import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -20,17 +22,28 @@ public class FeatureToggleRepositoryTest {
 
     @Test
     public void no_backup_file_and_no_repository_available_should_give_empty_repo() {
-        ToggleFetcher toggleFetcher = new HttpToggleFetcher(URI.create("http://localhost:4242/features"));
+        UnleashConfig config = UnleashConfig.builder()
+                .appName("test")
+                .unleashAPI("http://localhost:4242/").build();
+        ToggleFetcher toggleFetcher = new HttpToggleFetcher(config);
         ToggleBackupHandler toggleBackupHandler = new ToggleBackupHandlerFile();
-        ToggleRepository toggleRepository = new FeatureToggleRepository(toggleFetcher, toggleBackupHandler);
+        UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
+        ToggleRepository toggleRepository = new FeatureToggleRepository(config, executor, toggleFetcher, toggleBackupHandler);
         assertNull("should be null", toggleRepository.getToggle("unknownFeature"));
     }
 
     @Test
     public void backup_toggles_should_be_loaded_at_startup() {
+        UnleashConfig config = UnleashConfig.builder()
+                .appName("test")
+                .unleashAPI("http://localhost:4242/")
+                .fetchTogglesInterval(Long.MAX_VALUE)
+                .build();
+
         ToggleBackupHandler toggleBackupHandler = mock(ToggleBackupHandler.class);
         ToggleFetcher toggleFetcher = mock(ToggleFetcher.class);
-        new FeatureToggleRepository(toggleFetcher, toggleBackupHandler, Long.MAX_VALUE);
+        UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
+        new FeatureToggleRepository(config, executor, toggleFetcher, toggleBackupHandler);
 
         verify(toggleBackupHandler, times(1)).read();
     }
@@ -48,13 +61,26 @@ public class FeatureToggleRepositoryTest {
         //setup fetcher
         toggleCollection = populatedToggleCollection(
                 new FeatureToggle("toggleFetcherCalled", true, Arrays.asList(new ActivationStrategy("custom", null))));
-        Response response = new Response(Response.Status.CHANGED, toggleCollection);
+        FeatureToggleResponse response = new FeatureToggleResponse(FeatureToggleResponse.Status.CHANGED, toggleCollection);
         when(toggleFetcher.fetchToggles()).thenReturn(response);
 
         //init
-        ToggleRepository toggleRepository = new FeatureToggleRepository(toggleFetcher, toggleBackupHandler, 200L);
+        UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
+        ArgumentCaptor<Runnable> runnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class);
+
+
+        UnleashConfig config = new UnleashConfig.Builder()
+                .appName("test")
+                .unleashAPI("http://localhost:4242")
+                .fetchTogglesInterval(200l)
+                .build();
+
+        ToggleRepository toggleRepository = new FeatureToggleRepository(config, executor, toggleFetcher, toggleBackupHandler);
 
-        Thread.sleep(250L); //wait for background fetching
+        //run the toggle fetcher callback
+        verify(executor).setInterval(runnableArgumentCaptor.capture(), anyLong(), anyLong());
+        verify(toggleFetcher, times(0)).fetchToggles();
+        runnableArgumentCaptor.getValue().run();
 
         verify(toggleBackupHandler, times(1)).read();
         verify(toggleFetcher, times(1)).fetchToggles();
diff --git a/src/test/java/no/finn/unleash/repository/HttpToggleFetcherTest.java b/src/test/java/no/finn/unleash/repository/HttpToggleFetcherTest.java
index f0274912..296eb921 100644
--- a/src/test/java/no/finn/unleash/repository/HttpToggleFetcherTest.java
+++ b/src/test/java/no/finn/unleash/repository/HttpToggleFetcherTest.java
@@ -3,6 +3,7 @@ package no.finn.unleash.repository;
 import com.github.tomakehurst.wiremock.junit.WireMockRule;
 import no.finn.unleash.FeatureToggle;
 import no.finn.unleash.UnleashException;
+import no.finn.unleash.util.UnleashConfig;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
@@ -20,6 +21,7 @@ public class HttpToggleFetcherTest {
     @Rule
     public ExpectedException exception = ExpectedException.none();
 
+    /*
     @Test
     public void uri_is_not_absoulute() throws URISyntaxException {
         URI badUri = new URI("notAbsolute");
@@ -36,6 +38,7 @@ public class HttpToggleFetcherTest {
         exception.expect(UnleashException.class);
         new HttpToggleFetcher(badUrl);
     }
+    */
 
     @Test
     public void happy_path_test_version0() throws URISyntaxException {
@@ -46,9 +49,10 @@ public class HttpToggleFetcherTest {
                         .withHeader("Content-Type", "application/json")
                         .withBodyFile("features-v0.json")));
 
-        URI uri = new URI("http://localhost:"+wireMockRule.port()+ "/features");
-        HttpToggleFetcher httpToggleFetcher = new HttpToggleFetcher(uri);
-        Response response = httpToggleFetcher.fetchToggles();
+        URI uri = new URI("http://localhost:"+wireMockRule.port());
+        UnleashConfig config = UnleashConfig.builder().appName("test").unleashAPI(uri).build();
+        HttpToggleFetcher httpToggleFetcher = new HttpToggleFetcher(config);
+        FeatureToggleResponse response = httpToggleFetcher.fetchToggles();
         FeatureToggle featureX = response.getToggleCollection().getToggle("featureX");
 
         assertTrue(featureX.isEnabled());
@@ -66,9 +70,10 @@ public class HttpToggleFetcherTest {
                         .withHeader("Content-Type", "application/json")
                         .withBodyFile("features-v1.json")));
 
-        URI uri = new URI("http://localhost:"+wireMockRule.port()+ "/features");
-        HttpToggleFetcher httpToggleFetcher = new HttpToggleFetcher(uri);
-        Response response = httpToggleFetcher.fetchToggles();
+        URI uri = new URI("http://localhost:"+wireMockRule.port());
+        UnleashConfig config = UnleashConfig.builder().appName("test").unleashAPI(uri).build();
+        HttpToggleFetcher httpToggleFetcher = new HttpToggleFetcher(config);
+        FeatureToggleResponse response = httpToggleFetcher.fetchToggles();
         FeatureToggle featureX = response.getToggleCollection().getToggle("featureX");
 
         assertTrue(featureX.isEnabled());
@@ -86,8 +91,9 @@ public class HttpToggleFetcherTest {
                         .withStatus(200)
                         .withHeader("Content-Type", "application/json")));
 
-        URI uri = new URI("http://localhost:"+wireMockRule.port()+ "/features");
-        HttpToggleFetcher httpToggleFetcher = new HttpToggleFetcher(uri);
+        URI uri = new URI("http://localhost:"+wireMockRule.port());
+        UnleashConfig config = UnleashConfig.builder().appName("test").unleashAPI(uri).build();
+        HttpToggleFetcher httpToggleFetcher = new HttpToggleFetcher(config);
         exception.expect(UnleashException.class);
         httpToggleFetcher.fetchToggles();
 
@@ -105,8 +111,9 @@ public class HttpToggleFetcherTest {
                         .withHeader("Content-Type", "application/json")
                         .withBody("{}")));
 
-        URI uri = new URI("http://localhost:"+wireMockRule.port()+ "/features");
-        HttpToggleFetcher httpToggleFetcher = new HttpToggleFetcher(uri);
+        URI uri = new URI("http://localhost:"+wireMockRule.port());
+        UnleashConfig config = UnleashConfig.builder().appName("test").unleashAPI(uri).build();
+        HttpToggleFetcher httpToggleFetcher = new HttpToggleFetcher(config);
         exception.expect(UnleashException.class);
         httpToggleFetcher.fetchToggles();
 
@@ -122,10 +129,11 @@ public class HttpToggleFetcherTest {
                         .withStatus(302)
                         .withHeader("Content-Type", "application/json")));
 
-        URI uri = new URI("http://localhost:"+wireMockRule.port()+ "/features");
-        HttpToggleFetcher httpToggleFetcher = new HttpToggleFetcher(uri);
-        Response response = httpToggleFetcher.fetchToggles();
-        assertEquals("Should return status NOT_CHANGED", response.getStatus(), Response.Status.NOT_CHANGED);
+        URI uri = new URI("http://localhost:"+wireMockRule.port());
+        UnleashConfig config = UnleashConfig.builder().appName("test").unleashAPI(uri).build();
+        HttpToggleFetcher httpToggleFetcher = new HttpToggleFetcher(config);
+        FeatureToggleResponse response = httpToggleFetcher.fetchToggles();
+        assertEquals("Should return status NOT_CHANGED", response.getStatus(), FeatureToggleResponse.Status.NOT_CHANGED);
 
         verify(getRequestedFor(urlMatching("/features"))
                 .withHeader("Content-Type", matching("application/json")));
@@ -142,10 +150,11 @@ public class HttpToggleFetcherTest {
                             .withStatus(httpCode)
                             .withHeader("Content-Type", "application/json")));
 
-            URI uri = new URI("http://localhost:" + wireMockRule.port() + "/features");
-            HttpToggleFetcher httpToggleFetcher = new HttpToggleFetcher(uri);
-            Response response = httpToggleFetcher.fetchToggles();
-            assertEquals("Should return status NOT_CHANGED", response.getStatus(), Response.Status.NOT_CHANGED);
+            URI uri = new URI("http://localhost:" + wireMockRule.port());
+            UnleashConfig config = UnleashConfig.builder().appName("test").unleashAPI(uri).build();
+            HttpToggleFetcher httpToggleFetcher = new HttpToggleFetcher(config);
+            FeatureToggleResponse response = httpToggleFetcher.fetchToggles();
+            assertEquals("Should return status NOT_CHANGED", response.getStatus(), FeatureToggleResponse.Status.NOT_CHANGED);
 
             verify(getRequestedFor(urlMatching("/features"))
                     .withHeader("Content-Type", matching("application/json")));
diff --git a/src/test/java/no/finn/unleash/util/UnleashConfigTest.java b/src/test/java/no/finn/unleash/util/UnleashConfigTest.java
new file mode 100644
index 00000000..7c9d1d88
--- /dev/null
+++ b/src/test/java/no/finn/unleash/util/UnleashConfigTest.java
@@ -0,0 +1,40 @@
+package no.finn.unleash.util;
+
+import org.junit.Test;
+
+import java.net.URI;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+public class UnleashConfigTest {
+
+    @Test(expected = IllegalStateException.class)
+    public void should_require_unleasAPI_url() {
+        UnleashConfig.builder().appName("test").build();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void should_require_app_name() {
+        UnleashConfig.builder().unleashAPI("http://unleash.com").build();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void should_require_valid_uri() {
+        UnleashConfig.builder().unleashAPI("this is not a uri").build();
+    }
+
+    @Test
+    public void should_build_config() {
+        UnleashConfig config = UnleashConfig.builder()
+                .appName("my-app")
+                .instanceId("my-instance-1")
+                .unleashAPI("http://unleash.org")
+                .build();
+
+        assertThat(config.getAppName(), is("my-app"));
+        assertThat(config.getInstanceId(), is("my-instance-1"));
+        assertThat(config.getUnleashAPI(), is(URI.create("http://unleash.org")));
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/java/no/finn/unleash/util/UnleashURLsTest.java b/src/test/java/no/finn/unleash/util/UnleashURLsTest.java
new file mode 100644
index 00000000..3dfb0de3
--- /dev/null
+++ b/src/test/java/no/finn/unleash/util/UnleashURLsTest.java
@@ -0,0 +1,41 @@
+package no.finn.unleash.util;
+
+import org.junit.Test;
+
+import java.net.URI;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+
+public class UnleashURLsTest {
+
+
+    @Test
+    public void should_handle_additional_slash() {
+        UnleashURLs urls = new UnleashURLs(URI.create("http://unleash.com/"));
+        assertThat(urls.getFetchTogglesURL().toString(), is("http://unleash.com/features"));
+    }
+
+    @Test
+    public void should_set_correct_client_register_url() {
+        UnleashURLs urls = new UnleashURLs(URI.create("http://unleash.com"));
+        assertThat(urls.getClientRegisterURL().toString(), is("http://unleash.com/client/register"));
+    }
+
+    @Test
+    public void should_set_correct_client_metrics_url() {
+        UnleashURLs urls = new UnleashURLs(URI.create("http://unleash.com"));
+        assertThat(urls.getClientMetricsURL().toString(), is("http://unleash.com/client/metrics"));
+    }
+
+    @Test
+    public void should_set_correct_fetch_url() {
+        UnleashURLs urls = new UnleashURLs(URI.create("http://unleash.com"));
+        assertThat(urls.getFetchTogglesURL().toString(), is("http://unleash.com/features"));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void should_throw() {
+        new UnleashURLs(URI.create("unleash.com"));
+    }
+}
\ No newline at end of file