diff --git a/spring-boot/feature-flags/pom.xml b/spring-boot/feature-flags/pom.xml
index 7c1bf86..3e864ca 100644
--- a/spring-boot/feature-flags/pom.xml
+++ b/spring-boot/feature-flags/pom.xml
@@ -21,10 +21,18 @@
org.springframework.boot
spring-boot-starter-web
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
org.springframework.boot
spring-boot-starter-thymeleaf
+
+ com.h2database
+ h2
+
diff --git a/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/FeatureFlagService.java b/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/FeatureFlagService.java
new file mode 100644
index 0000000..f6e5ae1
--- /dev/null
+++ b/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/FeatureFlagService.java
@@ -0,0 +1,9 @@
+package io.reflectoring.featureflags.implementations;
+
+public interface FeatureFlagService {
+
+ Boolean featureOne();
+
+ Integer featureTwo();
+
+}
diff --git a/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/code/CodeBackedFeatureFlagService.java b/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/code/CodeBackedFeatureFlagService.java
new file mode 100644
index 0000000..aaeef0a
--- /dev/null
+++ b/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/code/CodeBackedFeatureFlagService.java
@@ -0,0 +1,15 @@
+package io.reflectoring.featureflags.implementations.code;
+
+import io.reflectoring.featureflags.implementations.FeatureFlagService;
+
+public class CodeBackedFeatureFlagService implements FeatureFlagService {
+ @Override
+ public Boolean featureOne() {
+ return true;
+ }
+
+ @Override
+ public Integer featureTwo() {
+ return 42;
+ }
+}
diff --git a/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/contextsensitive/ContextSensitiveFeatureFlagService.java b/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/contextsensitive/ContextSensitiveFeatureFlagService.java
new file mode 100644
index 0000000..0f224fe
--- /dev/null
+++ b/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/contextsensitive/ContextSensitiveFeatureFlagService.java
@@ -0,0 +1,52 @@
+package io.reflectoring.featureflags.implementations.contextsensitive;
+
+import io.reflectoring.featureflags.implementations.FeatureFlagService;
+import io.reflectoring.featureflags.implementations.contextsensitive.Feature.RolloutStrategy;
+import io.reflectoring.featureflags.web.UserSession;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+public class ContextSensitiveFeatureFlagService implements FeatureFlagService {
+
+ private final JdbcTemplate jdbcTemplate;
+ private final UserSession userSession;
+
+ public ContextSensitiveFeatureFlagService(JdbcTemplate jdbcTemplate, UserSession userSession) {
+ this.jdbcTemplate = jdbcTemplate;
+ this.userSession = userSession;
+ }
+
+ @Override
+ public Boolean featureOne() {
+ Feature feature = getFeatureFromDatabase();
+ if (feature == null) {
+ return Boolean.FALSE;
+ }
+ return feature.evaluateBoolean(userSession.getUsername());
+ }
+
+ @Override
+ public Integer featureTwo() {
+ Feature feature = getFeatureFromDatabase();
+ if (feature == null) {
+ return null;
+ }
+ return feature.evaluateInt(userSession.getUsername());
+ }
+
+ @Nullable
+ private Feature getFeatureFromDatabase() {
+ return jdbcTemplate.query("select targeting, value, defaultValue, percentage from features where feature_key='FEATURE_ONE'", resultSet -> {
+ if (!resultSet.next()) {
+ return null;
+ }
+
+ RolloutStrategy rolloutStrategy = Enum.valueOf(RolloutStrategy.class, resultSet.getString(1));
+ String value = resultSet.getString(2);
+ String defaultValue = resultSet.getString(3);
+ int percentage = resultSet.getInt(4);
+
+ return new Feature(rolloutStrategy, value, defaultValue, percentage);
+ });
+ }
+}
diff --git a/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/contextsensitive/Feature.java b/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/contextsensitive/Feature.java
new file mode 100644
index 0000000..b1d74e7
--- /dev/null
+++ b/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/contextsensitive/Feature.java
@@ -0,0 +1,102 @@
+package io.reflectoring.featureflags.implementations.contextsensitive;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+public class Feature {
+
+ public enum RolloutStrategy {
+ GLOBAL,
+ PERCENTAGE;
+ }
+
+ private final RolloutStrategy rolloutStrategy;
+
+ private final int percentage;
+ private final String value;
+ private final String defaultValue;
+
+ public Feature(RolloutStrategy rolloutStrategy, String value, String defaultValue, int percentage) {
+ this.rolloutStrategy = rolloutStrategy;
+ this.percentage = percentage;
+ this.value = value;
+ this.defaultValue = defaultValue;
+ }
+
+ public boolean evaluateBoolean(String userId) {
+ switch (this.rolloutStrategy) {
+ case GLOBAL:
+ return this.getBooleanValue();
+ case PERCENTAGE:
+ if (percentageHashCode(userId) <= this.percentage) {
+ return this.getBooleanValue();
+ } else {
+ return this.getBooleanDefaultValue();
+ }
+ }
+
+ return this.getBooleanDefaultValue();
+ }
+
+ public Integer evaluateInt(String userId) {
+ switch (this.rolloutStrategy) {
+ case GLOBAL:
+ return this.getIntValue();
+ case PERCENTAGE:
+ if (percentageHashCode(userId) <= this.percentage) {
+ return this.getIntValue();
+ } else {
+ return this.getIntDefaultValue();
+ }
+ }
+
+ return this.getIntDefaultValue();
+ }
+
+ double percentageHashCode(String text) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] encodedhash = digest.digest(
+ text.getBytes(StandardCharsets.UTF_8));
+ double INTEGER_RANGE = 1L << 32;
+ return (((long) Arrays.hashCode(encodedhash) - Integer.MIN_VALUE) / INTEGER_RANGE) * 100;
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public RolloutStrategy getTargeting() {
+ return rolloutStrategy;
+ }
+
+ public int getPercentage() {
+ return percentage;
+ }
+
+ public int getIntValue() {
+ return Integer.parseInt(this.value);
+ }
+
+ public int getIntDefaultValue() {
+ return Integer.parseInt(this.defaultValue);
+ }
+
+
+ public boolean getBooleanValue() {
+ return Boolean.parseBoolean(this.value);
+ }
+
+ public boolean getBooleanDefaultValue() {
+ return Boolean.parseBoolean(this.defaultValue);
+ }
+
+ public String getStringValue() {
+ return value;
+ }
+
+ public String getDefaultValue() {
+ return defaultValue;
+ }
+}
diff --git a/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/database/DatabaseBackedFeatureFlagService.java b/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/database/DatabaseBackedFeatureFlagService.java
new file mode 100644
index 0000000..2b4cb8b
--- /dev/null
+++ b/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/database/DatabaseBackedFeatureFlagService.java
@@ -0,0 +1,32 @@
+package io.reflectoring.featureflags.implementations.database;
+
+import io.reflectoring.featureflags.implementations.FeatureFlagService;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+public class DatabaseBackedFeatureFlagService implements FeatureFlagService {
+
+ private JdbcTemplate jdbcTemplate;
+
+ @Override
+ public Boolean featureOne() {
+ return jdbcTemplate.query("select value from features where feature_key='FEATURE_ONE'", resultSet -> {
+ if(!resultSet.next()){
+ return false;
+ }
+
+ boolean value = Boolean.parseBoolean(resultSet.getString(1));
+ return value ? Boolean.TRUE : Boolean.FALSE;
+ });
+ }
+
+ @Override
+ public Integer featureTwo() {
+ return jdbcTemplate.query("select value from features where feature_key='FEATURE_TWO'", resultSet -> {
+ if(!resultSet.next()){
+ return null;
+ }
+
+ return Integer.valueOf(resultSet.getString(1));
+ });
+ }
+}
diff --git a/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/launchdarkly/LaunchDarklyFeatureFlagService.java b/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/launchdarkly/LaunchDarklyFeatureFlagService.java
new file mode 100644
index 0000000..f99bfce
--- /dev/null
+++ b/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/launchdarkly/LaunchDarklyFeatureFlagService.java
@@ -0,0 +1,32 @@
+package io.reflectoring.featureflags.implementations.launchdarkly;
+
+import com.launchdarkly.sdk.LDUser;
+import com.launchdarkly.sdk.server.LDClient;
+import io.reflectoring.featureflags.implementations.FeatureFlagService;
+import io.reflectoring.featureflags.web.UserSession;
+
+public class LaunchDarklyFeatureFlagService implements FeatureFlagService {
+
+ private final LDClient launchdarklyClient;
+ private final UserSession userSession;
+
+ public LaunchDarklyFeatureFlagService(LDClient launchdarklyClient, UserSession userSession) {
+ this.launchdarklyClient = launchdarklyClient;
+ this.userSession = userSession;
+ }
+
+ @Override
+ public Boolean featureOne() {
+ return launchdarklyClient.boolVariation("feature-one", getLaunchdarklyUserFromSession(), false);
+ }
+
+ @Override
+ public Integer featureTwo() {
+ return launchdarklyClient.intVariation("feature-two", getLaunchdarklyUserFromSession(), 0);
+ }
+
+ private LDUser getLaunchdarklyUserFromSession() {
+ return new LDUser.Builder(userSession.getUsername())
+ .build();
+ }
+}
diff --git a/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/properties/FeatureProperties.java b/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/properties/FeatureProperties.java
new file mode 100644
index 0000000..7df00a4
--- /dev/null
+++ b/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/properties/FeatureProperties.java
@@ -0,0 +1,31 @@
+package io.reflectoring.featureflags.implementations.properties;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Component
+@ConfigurationProperties("features")
+public class FeatureProperties {
+
+ private boolean featureOne;
+ private int featureTwo;
+
+ public FeatureProperties() {
+ }
+
+ public boolean getFeatureOne() {
+ return featureOne;
+ }
+
+ public void setFeatureOne(boolean featureOne) {
+ this.featureOne = featureOne;
+ }
+
+ public int getFeatureTwo() {
+ return featureTwo;
+ }
+
+ public void setFeatureTwo(int featureTwo) {
+ this.featureTwo = featureTwo;
+ }
+}
diff --git a/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/properties/PropertiesBackedFeatureFlagService.java b/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/properties/PropertiesBackedFeatureFlagService.java
new file mode 100644
index 0000000..2d596b4
--- /dev/null
+++ b/spring-boot/feature-flags/src/main/java/io/reflectoring/featureflags/implementations/properties/PropertiesBackedFeatureFlagService.java
@@ -0,0 +1,25 @@
+package io.reflectoring.featureflags.implementations.properties;
+
+
+import io.reflectoring.featureflags.implementations.FeatureFlagService;
+import org.springframework.stereotype.Component;
+
+@Component
+public class PropertiesBackedFeatureFlagService implements FeatureFlagService {
+
+ private final FeatureProperties featureProperties;
+
+ public PropertiesBackedFeatureFlagService(FeatureProperties featureProperties) {
+ this.featureProperties = featureProperties;
+ }
+
+ @Override
+ public Boolean featureOne() {
+ return featureProperties.getFeatureOne();
+ }
+
+ @Override
+ public Integer featureTwo() {
+ return featureProperties.getFeatureTwo();
+ }
+}
diff --git a/spring-boot/feature-flags/src/main/resources/application.yml b/spring-boot/feature-flags/src/main/resources/application.yml
index 091bb99..f2b0c7a 100644
--- a/spring-boot/feature-flags/src/main/resources/application.yml
+++ b/spring-boot/feature-flags/src/main/resources/application.yml
@@ -15,4 +15,8 @@ togglz:
enabled: true
secured: false
path: /togglz
- use-management-port: false
\ No newline at end of file
+ use-management-port: false
+
+features:
+ featureOne: true
+ featureTwo: 42
\ No newline at end of file
diff --git a/spring-boot/feature-flags/src/test/java/io/reflectoring/featureflags/implementations/contextsensitive/FeatureTest.java b/spring-boot/feature-flags/src/test/java/io/reflectoring/featureflags/implementations/contextsensitive/FeatureTest.java
new file mode 100644
index 0000000..c86b0f6
--- /dev/null
+++ b/spring-boot/feature-flags/src/test/java/io/reflectoring/featureflags/implementations/contextsensitive/FeatureTest.java
@@ -0,0 +1,19 @@
+package io.reflectoring.featureflags.implementations.contextsensitive;
+
+import io.reflectoring.featureflags.implementations.contextsensitive.Feature.RolloutStrategy;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.offset;
+
+public class FeatureTest {
+
+ @Test
+ void testHashCode(){
+ Feature feature = new Feature(RolloutStrategy.PERCENTAGE, "true", "false", 50);
+ assertThat(feature.percentageHashCode("1")).isCloseTo(27.74d, offset(0.01d));
+ assertThat(feature.percentageHashCode("2")).isCloseTo(81.12d, offset(0.01d));
+ assertThat(feature.percentageHashCode("3")).isCloseTo(21.69d, offset(0.01d));
+ }
+
+}