Skip to content

Commit e4d52aa

Browse files
committed
example feature flag implementations
1 parent 7520c38 commit e4d52aa

File tree

11 files changed

+330
-1
lines changed

11 files changed

+330
-1
lines changed

spring-boot/feature-flags/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,18 @@
2121
<groupId>org.springframework.boot</groupId>
2222
<artifactId>spring-boot-starter-web</artifactId>
2323
</dependency>
24+
<dependency>
25+
<groupId>org.springframework.boot</groupId>
26+
<artifactId>spring-boot-starter-jdbc</artifactId>
27+
</dependency>
2428
<dependency>
2529
<groupId>org.springframework.boot</groupId>
2630
<artifactId>spring-boot-starter-thymeleaf</artifactId>
2731
</dependency>
32+
<dependency>
33+
<groupId>com.h2database</groupId>
34+
<artifactId>h2</artifactId>
35+
</dependency>
2836

2937
<!-- TOGGLZ -->
3038
<dependency>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.reflectoring.featureflags.implementations;
2+
3+
public interface FeatureFlagService {
4+
5+
Boolean featureOne();
6+
7+
Integer featureTwo();
8+
9+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.reflectoring.featureflags.implementations.code;
2+
3+
import io.reflectoring.featureflags.implementations.FeatureFlagService;
4+
5+
public class CodeBackedFeatureFlagService implements FeatureFlagService {
6+
@Override
7+
public Boolean featureOne() {
8+
return true;
9+
}
10+
11+
@Override
12+
public Integer featureTwo() {
13+
return 42;
14+
}
15+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package io.reflectoring.featureflags.implementations.contextsensitive;
2+
3+
import io.reflectoring.featureflags.implementations.FeatureFlagService;
4+
import io.reflectoring.featureflags.implementations.contextsensitive.Feature.RolloutStrategy;
5+
import io.reflectoring.featureflags.web.UserSession;
6+
import org.jetbrains.annotations.Nullable;
7+
import org.springframework.jdbc.core.JdbcTemplate;
8+
9+
public class ContextSensitiveFeatureFlagService implements FeatureFlagService {
10+
11+
private final JdbcTemplate jdbcTemplate;
12+
private final UserSession userSession;
13+
14+
public ContextSensitiveFeatureFlagService(JdbcTemplate jdbcTemplate, UserSession userSession) {
15+
this.jdbcTemplate = jdbcTemplate;
16+
this.userSession = userSession;
17+
}
18+
19+
@Override
20+
public Boolean featureOne() {
21+
Feature feature = getFeatureFromDatabase();
22+
if (feature == null) {
23+
return Boolean.FALSE;
24+
}
25+
return feature.evaluateBoolean(userSession.getUsername());
26+
}
27+
28+
@Override
29+
public Integer featureTwo() {
30+
Feature feature = getFeatureFromDatabase();
31+
if (feature == null) {
32+
return null;
33+
}
34+
return feature.evaluateInt(userSession.getUsername());
35+
}
36+
37+
@Nullable
38+
private Feature getFeatureFromDatabase() {
39+
return jdbcTemplate.query("select targeting, value, defaultValue, percentage from features where feature_key='FEATURE_ONE'", resultSet -> {
40+
if (!resultSet.next()) {
41+
return null;
42+
}
43+
44+
RolloutStrategy rolloutStrategy = Enum.valueOf(RolloutStrategy.class, resultSet.getString(1));
45+
String value = resultSet.getString(2);
46+
String defaultValue = resultSet.getString(3);
47+
int percentage = resultSet.getInt(4);
48+
49+
return new Feature(rolloutStrategy, value, defaultValue, percentage);
50+
});
51+
}
52+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package io.reflectoring.featureflags.implementations.contextsensitive;
2+
3+
import java.nio.charset.StandardCharsets;
4+
import java.security.MessageDigest;
5+
import java.security.NoSuchAlgorithmException;
6+
import java.util.Arrays;
7+
8+
public class Feature {
9+
10+
public enum RolloutStrategy {
11+
GLOBAL,
12+
PERCENTAGE;
13+
}
14+
15+
private final RolloutStrategy rolloutStrategy;
16+
17+
private final int percentage;
18+
private final String value;
19+
private final String defaultValue;
20+
21+
public Feature(RolloutStrategy rolloutStrategy, String value, String defaultValue, int percentage) {
22+
this.rolloutStrategy = rolloutStrategy;
23+
this.percentage = percentage;
24+
this.value = value;
25+
this.defaultValue = defaultValue;
26+
}
27+
28+
public boolean evaluateBoolean(String userId) {
29+
switch (this.rolloutStrategy) {
30+
case GLOBAL:
31+
return this.getBooleanValue();
32+
case PERCENTAGE:
33+
if (percentageHashCode(userId) <= this.percentage) {
34+
return this.getBooleanValue();
35+
} else {
36+
return this.getBooleanDefaultValue();
37+
}
38+
}
39+
40+
return this.getBooleanDefaultValue();
41+
}
42+
43+
public Integer evaluateInt(String userId) {
44+
switch (this.rolloutStrategy) {
45+
case GLOBAL:
46+
return this.getIntValue();
47+
case PERCENTAGE:
48+
if (percentageHashCode(userId) <= this.percentage) {
49+
return this.getIntValue();
50+
} else {
51+
return this.getIntDefaultValue();
52+
}
53+
}
54+
55+
return this.getIntDefaultValue();
56+
}
57+
58+
double percentageHashCode(String text) {
59+
try {
60+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
61+
byte[] encodedhash = digest.digest(
62+
text.getBytes(StandardCharsets.UTF_8));
63+
double INTEGER_RANGE = 1L << 32;
64+
return (((long) Arrays.hashCode(encodedhash) - Integer.MIN_VALUE) / INTEGER_RANGE) * 100;
65+
} catch (NoSuchAlgorithmException e) {
66+
throw new IllegalStateException(e);
67+
}
68+
}
69+
70+
public RolloutStrategy getTargeting() {
71+
return rolloutStrategy;
72+
}
73+
74+
public int getPercentage() {
75+
return percentage;
76+
}
77+
78+
public int getIntValue() {
79+
return Integer.parseInt(this.value);
80+
}
81+
82+
public int getIntDefaultValue() {
83+
return Integer.parseInt(this.defaultValue);
84+
}
85+
86+
87+
public boolean getBooleanValue() {
88+
return Boolean.parseBoolean(this.value);
89+
}
90+
91+
public boolean getBooleanDefaultValue() {
92+
return Boolean.parseBoolean(this.defaultValue);
93+
}
94+
95+
public String getStringValue() {
96+
return value;
97+
}
98+
99+
public String getDefaultValue() {
100+
return defaultValue;
101+
}
102+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.reflectoring.featureflags.implementations.database;
2+
3+
import io.reflectoring.featureflags.implementations.FeatureFlagService;
4+
import org.springframework.jdbc.core.JdbcTemplate;
5+
6+
public class DatabaseBackedFeatureFlagService implements FeatureFlagService {
7+
8+
private JdbcTemplate jdbcTemplate;
9+
10+
@Override
11+
public Boolean featureOne() {
12+
return jdbcTemplate.query("select value from features where feature_key='FEATURE_ONE'", resultSet -> {
13+
if(!resultSet.next()){
14+
return false;
15+
}
16+
17+
boolean value = Boolean.parseBoolean(resultSet.getString(1));
18+
return value ? Boolean.TRUE : Boolean.FALSE;
19+
});
20+
}
21+
22+
@Override
23+
public Integer featureTwo() {
24+
return jdbcTemplate.query("select value from features where feature_key='FEATURE_TWO'", resultSet -> {
25+
if(!resultSet.next()){
26+
return null;
27+
}
28+
29+
return Integer.valueOf(resultSet.getString(1));
30+
});
31+
}
32+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.reflectoring.featureflags.implementations.launchdarkly;
2+
3+
import com.launchdarkly.sdk.LDUser;
4+
import com.launchdarkly.sdk.server.LDClient;
5+
import io.reflectoring.featureflags.implementations.FeatureFlagService;
6+
import io.reflectoring.featureflags.web.UserSession;
7+
8+
public class LaunchDarklyFeatureFlagService implements FeatureFlagService {
9+
10+
private final LDClient launchdarklyClient;
11+
private final UserSession userSession;
12+
13+
public LaunchDarklyFeatureFlagService(LDClient launchdarklyClient, UserSession userSession) {
14+
this.launchdarklyClient = launchdarklyClient;
15+
this.userSession = userSession;
16+
}
17+
18+
@Override
19+
public Boolean featureOne() {
20+
return launchdarklyClient.boolVariation("feature-one", getLaunchdarklyUserFromSession(), false);
21+
}
22+
23+
@Override
24+
public Integer featureTwo() {
25+
return launchdarklyClient.intVariation("feature-two", getLaunchdarklyUserFromSession(), 0);
26+
}
27+
28+
private LDUser getLaunchdarklyUserFromSession() {
29+
return new LDUser.Builder(userSession.getUsername())
30+
.build();
31+
}
32+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package io.reflectoring.featureflags.implementations.properties;
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties;
4+
import org.springframework.stereotype.Component;
5+
6+
@Component
7+
@ConfigurationProperties("features")
8+
public class FeatureProperties {
9+
10+
private boolean featureOne;
11+
private int featureTwo;
12+
13+
public FeatureProperties() {
14+
}
15+
16+
public boolean getFeatureOne() {
17+
return featureOne;
18+
}
19+
20+
public void setFeatureOne(boolean featureOne) {
21+
this.featureOne = featureOne;
22+
}
23+
24+
public int getFeatureTwo() {
25+
return featureTwo;
26+
}
27+
28+
public void setFeatureTwo(int featureTwo) {
29+
this.featureTwo = featureTwo;
30+
}
31+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.reflectoring.featureflags.implementations.properties;
2+
3+
4+
import io.reflectoring.featureflags.implementations.FeatureFlagService;
5+
import org.springframework.stereotype.Component;
6+
7+
@Component
8+
public class PropertiesBackedFeatureFlagService implements FeatureFlagService {
9+
10+
private final FeatureProperties featureProperties;
11+
12+
public PropertiesBackedFeatureFlagService(FeatureProperties featureProperties) {
13+
this.featureProperties = featureProperties;
14+
}
15+
16+
@Override
17+
public Boolean featureOne() {
18+
return featureProperties.getFeatureOne();
19+
}
20+
21+
@Override
22+
public Integer featureTwo() {
23+
return featureProperties.getFeatureTwo();
24+
}
25+
}

spring-boot/feature-flags/src/main/resources/application.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,8 @@ togglz:
1515
enabled: true
1616
secured: false
1717
path: /togglz
18-
use-management-port: false
18+
use-management-port: false
19+
20+
features:
21+
featureOne: true
22+
featureTwo: 42

0 commit comments

Comments
 (0)