I was facing a situation at work the other day where I wanted to only show certain features in certain environments. We were still testing out a particular changeset and didn't want it visible to our users in production, but we didn't want to have long lived feature branches... what were we to do?
Introducing Feature Flags
A feature flag is basically a way of telling a chunk of code whether or not it should be enabled. You can turn on certain features when you're ready and quickly turn them off if need be.
These flags are usually extracted out of the code layer to configuration files or somewhere else that's easier to access/change. Doing this makes the flags able to be altered by your build pipeline.
Extracting Flags in Spring Boot
My weapon of choice is Java, more often than not I'm working with the Spring framework. It's basically ubiquitous in Java development now, so let's roll up some feature flags using it.
We'll put together two ways of serving up different features. One will use Spring Profiles in order to offer different beans based on where the code is being run. It could give you a different bean when run in your "development" instance versus your "production" environment.
The other way of going about enabling feature flags is using properties extracted to a flat file on the classpath. This would allow you to change the properties file and not have to affect the codebase at all.
Profile Dependent Beans
First we'll define an interface for our different environments. This will lay the groundwork for how we allow different implementations based on the specific profile used.
public interface Environment {
String getName();
default Boolean safeToTest() {
return Boolean.FALSE;
}
}
Simple, right? Let the environment define its own name. And then keep a flag to let us know if it's safe for us to test there. By default, assume it's not safe to test.
Then we'll call out a few different environments...
@Profile("development")
@Component
public class DevelopmentEnvironment implements Environment {
public static final String NAME = "Development Environment";
@Override
public String getName() {
return NAME;
}
@Override
public Boolean safeToTest() {
return Boolean.TRUE;
}
}
This bean implements the Environment
interface and lets us know that it's safe to test in the Development Environment
.
@Profile("production")
@Component
public class ProductionEnvironment implements Environment {
public static final String NAME = "Production Environment";
@Override
public String getName() {
return NAME;
}
}
It is obviously not safe to test in the Production Environment
, unless your Bill O'Reilly.
Now, because Spring Boot handles our dependency injection for us, we can ask for an implementation of the Environment
and we will be provided by the proper implementation, based on which profile has been passed by the build/runner.
@Service
public class DecisionMaker {
public static final String decisionFormat = "It is %ssafe to test on the %s.";
@Autowired
private Environment environment;
public String canWeTest() {
return format(decisionFormat, safeToTestString(environment.safeToTest()), environment.getName());
}
public static String safeToTestString(Boolean safeToTest) {
return safeToTest ? "" : "not ";
}
}
We can test this out using JUnit and specifying which profile to use in each of our test cases.
@SpringBootTest
@ActiveProfiles("development")
public class DevelopmentDecisionMakerTest {
@Autowired
private DecisionMaker decisionMaker;
@Test
void shouldUseDevelopmentEnvironment() {
String decision = decisionMaker.canWeTest();
assertThat(decision).as("Should describe the Development environment.")
.isEqualTo(format(DecisionMaker.decisionFormat,
DecisionMaker.safeToTestString(Boolean.TRUE),
DevelopmentEnvironment.NAME));
}
}
Check out the full test suite available in the GitHub Repo
Property Extraction
Another way of managing these feature flags is extracting out a list of enabled and disabled features to a list in your properties file. We can accomplish this via the use of ConfigurationProperties bindings. This allows us to map directly from our properties file on the classpath to a POJO.
In this case, I'm using a Record because they're fancy and I've not used them before. But, you could do this with a Lombok @Data or just a plain old bean.
@ConfigurationProperties("features")
public record FeaturesAvailable( List<String> enabled, List<String> disabled) {
@ConstructorBinding
public FeaturesAvailable(List<String> enabled, List<String> disabled) {
this.enabled = Optional.ofNullable(enabled).orElse(Collections.emptyList());
this.disabled = Optional.ofNullable(disabled).orElse(Collections.emptyList());
}
}
You'll notice I've done a bit of extra checking there, to allow for developers to forget to add the values to their properties file. I always find it safe to assume that everyone is as forgetful as I am. It's for the best.
Now that we have a FeaturesAvailable
configuration available, let's add that to our DecisionMaker
and see what we can do with it.
@Service
public class DecisionMaker {
@Autowired
private FeaturesAvailable features;
public List<String> availableFeatures() {
return features.enabled();
}
public List<String> betaFeatures() {
return features.disabled();
}
}
Simple enough, exposing the list of values that are available and those that are disabled.
Once again, we'll just put together a quick integration test to show that we can pass in the correct properties. We'll rely on our different profiles again in order to provide different sets of data and get a bit of fun out of our tests.
--------
spring:
config:
activate:
on-profile: test
features:
enabled:
- "Feature One"
- "Feature Two"
disabled:
- "Beta Feature"
--------
spring:
config:
activate:
on-profile: development
features:
enabled:
- "Feature One"
disabled:
- "Feature Two"
- "Beta Feature"
--------
spring:
config:
activate:
on-profile: production
features:
enabled:
disabled:
- "Feature One"
- "Feature Two"
- "Beta Feature"
We've gone through and specified different lists of enabled/disabled
based on the active profile. This allows us to write the following tests.
@SpringBootTest
@ActiveProfiles("production")
public class ProductionDecisionMakerTest {
@Autowired
private DecisionMaker decisionMaker;
@Test
void shouldReturnAvailableFeatures() {
List<String> availableFeatures = decisionMaker.availableFeatures();
assertThat(availableFeatures).isEmpty();
}
@Test
void shouldReturnADisabledFeatures() {
List<String> availableFeatures = decisionMaker.betaFeatures();
assertThat(availableFeatures).containsExactly("Feature One", "Feature Two", "Beta Feature");
}
}
And
@SpringBootTest
public class TestDecisionMakerTest {
@Autowired
private DecisionMaker decisionMaker;
@Test
void shouldReturnAvailableFeatures() {
List<String> availableFeatures = decisionMaker.availableFeatures();
assertThat(availableFeatures).containsExactly("Feature One", "Feature Two");
}
@Test
void shouldReturnADisabledFeatures() {
List<String> availableFeatures = decisionMaker.betaFeatures();
assertThat(availableFeatures).containsExactly("Beta Feature");
}
}
We very quickly see that the different profiles have different values exposed via the FeaturesAvailable
configuration!
Summary
I've provided two ways to implement feature flags that don't require a dedicated solution. There are more robust methods for doing this, but sometimes you just want/need to go lightweight.
If you'd like to see the full implementation, please check out the GitHub Repo.
Top comments (1)
Great lightweight implementation of feature flags. If you're looking to take feature flags further, check out open source project Flagsmith - github.com/Flagsmith/flagsmith