DEV Community

Cover image for Feature Flags in Spring Boot
Seth Kellas
Seth Kellas

Posted on

Feature Flags in Spring Boot

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;
    }

}
Enter fullscreen mode Exit fullscreen mode

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;
    }

}
Enter fullscreen mode Exit fullscreen mode

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;
    }

}
Enter fullscreen mode Exit fullscreen mode

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 ";
    }

}
Enter fullscreen mode Exit fullscreen mode

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));
    }

}
Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }

}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (1)

Collapse
alexeboswell profile image
Alex Boswell

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