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