*article updated on September 26, 2024
When it comes to agile development and "moving fast and breaking stuff," many people think of REST APIs. Decomposing services into manageable building blocks with clearly defined interfaces is a good step in designing any large system. Even if one doesn't necessarily wish to be "agile," REST microservices are an effective design pattern. But wouldn't it be nice to separate "move fast and break stuff" into two parts and throw the second one away? Feature toggles (also known as feature flags) can help with that.
They provide us with a way to add new behavior to an application and then enable or disable the new behavior without deploying a different version. Toggles give us immediate advantages when implementing a REST microservice. We can toggle new behavior for a subset of users or only enable it after deploying a new client application. So we add a feature toggle to our configuration file or the command line, default it to "off," and wait until later to flip it, right? Does that describe a complete solution? Not really.
Updating a configuration file isn't always more straightforward than deploying a new release, and adding a boolean flag for each new feature seems like a bit of a hack. However, there is a better way. There's feature flag management. Instead of using conditionals that will inevitably become technical debt, you can include these toggles as part of a strategy for improving your code and your ability to support it. Let's explore feature toggles in a REST service written with Spring Boot. We'll start with a simple flag and build it into a fully managed feature flag with CloudBees Feature Management's secure feature management system.
This tutorial assumes familiarity with basic Spring concepts and the ability to set up a development environment. While we will look at several Spring components, we'll leave it up to you whether you run your application from Gradle, Maven, IDE, etc.
What Are Feature Toggles?
We touched briefly on the definition of feature toggles, but let's present a more complete definition now.
Feature toggles are mechanisms that allow you to activate and deactivate portions of the codebase. You could think of a feature toggle as nothing more than a glorified if statement, and you wouldn't be that far off. However, the great advantage of toggles is that you can switch them without altering the code and redeploying the application. When you gain that kind of capability, your team can decouple the concepts of releases and deployments. Deployments become a non-issue and are made as frequently as possible.
A Basic Feature Toggle
Let's start with a straightforward Boot application. We have an interface that supplies a greeting.
public interface GreetingHandler { String getGreeting(String name); }
And a handler class that implements it.
public class StandardGreetingHandler implements GreetingHandler { private static final String template = "Hello, %s!"; public String getGreeting(String name) { return String.format(template, name); } }
We'll create a controller with a single request mapping.
@RestController public class GreetingController { private final GreetingHandler handler = new StandardGreetingHandler(); @RequestMapping("/greeting") public String greeting(@RequestParam(value="name", defaultValue="World") String name) { return handler.getGreeting(name); } }
Finally, there is the main class to run the service.
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
We can use Curl to test this service, but any test client will do.
$ curl -i http://localhost:8080/greeting HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8 Content-Length: 13 Date: Tue, 30 Aug 2022 21:11:04 GMT Hello, World!
Above, we see our response with the standard HTTP headers.
A Spring Feature Toggle
So, let's imagine we want to replace this greeting with a different one depending on the season. Spring already has tools for solving this problem. After all, dependency injection and programmatic configuration are two of the main reasons Spring has gained a lot of traction in the Java development community. First, we'll add a property to our configuration. We'll use application.properties for the sake of simplicity and add a new parameter to the file.
feature.toggle.holidaySeason=false
Next, we create a new implementation of our GreetingHandler.
public class HolidayGreetingHandler implements GreetingHandler { private static final String template = "Happy Holidays, %s!"; public String getGreeting(String name) { return String.format(template, name); } }
Then, we'll modify our controller to accept a handler via dependency injection.
@RestController public class GreetingController { private final GreetingHandler handler; @Autowired public GreetingController(GreetingHandler handler) { this.handler = handler; } @RequestMapping("/greeting") public String greeting(@RequestParam(value="name", defaultValue="World") String name) { return handler.getGreeting(name); } }
Lastly, we add configuration code to the main class.
@SpringBootApplication public class Application { @Bean @Primary @ConditionalOnProperty(prefix = "feature.toggle", name = "holidaySeason", havingValue="false") public GreetingHandler getStandardGreetingHandler() { return new StandardGreetingHandler(); } @Bean @ConditionalOnProperty(prefix = "feature.toggle", name = "holidaySeason", havingValue="true") public GreetingHandler getGreetingHandler() { return new HolidayGreetingHandler(); } public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
If holidaySeason is true, Spring overrides the primary bean, StandardGreetingHandler, with HolidayGreetingHandler. If we run this version with the current configuration, everything looks the same.
$ curl -i http://localhost:8080/greeting HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8 Content-Length: 13 Date: Tue, 30 Aug 2022 21:30:57 GMT Hello, World!
But when we set feature.toggle.holidaySeason=true and restart the application, we see the new greeting.
$ curl -i http://localhost:8080/greeting HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8 Content-Length: 22 Date: Tue, 30 Aug 2022 21:31:18 GMT Happy Holidays, World!
We've added our first feature flag. The value of feature.toggle.holidaySeason controls our application's behavior. We can load the desired controller by modifying the application configuration without deploying new code.
Improving Your Spring Feature Toggle Management
Spring Properties are, at best, a limited version of feature flag management. They require modifying and distributing configuration files to toggle features. Many organizations manage their configurations like software with version control, packaging, and testing. Changing configuration requires a release cycle, and it should. Let's look at a better way to manage feature toggles.
Getting Started With CloudBees Feature Management
First, you'll need to create a free CloudBees Feature Management account here. Once that's done, sign in to your CloudBees Feature Management account and create a new application.
Be sure to select Java and Java Server, as shown above. Next, you'll see instructions for adding CloudBees Feature Management to your application. Use the dropdowns to select Java and Java Server to see instructions like these:
Your application will be assigned a unique initialization key. Add the CloudBees Feature Management library to your dependencies. At the time of this writing, the current version of the rox-java-server library is 5.0.7. Here are my Gradle build dependencies for both Spring Boot and CloudBees Feature Management:
dependencies { compile("org.springframework.boot:spring-boot-starter-web") compile group: 'io.rollout.rox', name: 'rox-java-server', version: '5.0.6' testCompile('org.springframework.boot:spring-boot-starter-test') testCompile('com.jayway.jsonpath:json-path') }
Initialize the API
Now, we need to add the call to Rox.setup() to initialize the SDK with the application key. This requires some additional thought in a Spring application, though. CloudBees Feature Management's API is asynchronous. It uses okHttp to make REST requests for application information, including the feature toggles that we'll add later. If we attempt to access our feature toggles before initialization has been completed, we may get an incorrect value.
We need to ensure CloudBees Feature Management is completely initialized and that our feature toggles have been updated before Spring uses them to decide how to set up our application.
Building a Spring Component to Manage Feature Toggles
Let's create a new component and use it to initialize CloudBees Feature Management. It will have code to wait until the CloudBees Feature Management initialization is complete before the application finishes loading. Then, we'll continue to set the class up for managing flags.
@Component public class RolloutCondition extends SpringBootCondition { private static AtomicBoolean initialized = new AtomicBoolean(false); private static Log log = LogFactory.getLog(RolloutCondition.class); public RolloutCondition() { if (!initialized.get()) { initialized.set(true); initializeRox(); } } @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { return new ConditionOutcome(false, ""); } private void initializeRox() { CountDownLatch roxFirstFetch = new CountDownLatch(1); try { RoxOptions options = new RoxOptions.Builder() .withConfigurationFetchedHandler( arg0 -> { if (roxFirstFetch.getCount() > 0) { roxFirstFetch.countDown(); log.info("Retrieved Rollout configuration"); } }).build(); Rox.setup("5ab7cac73827af14484e440b", options); roxFirstFetch.await(10, TimeUnit.SECONDS); } catch (InterruptedException ie) { log.error("Interrupted waiting for rollout data."); } } }
This class adds two bits of functionality to our application. Rox.setup() accepts an optional RoxOptions class. We built one that installs CloudBees Feature Management API's ConfigurationFetchedHandler callback, which is called after CloudBees Feature Management's configuration is retrieved. The callback sets a CountDownLatch after the CloudBees Feature Management is fully initialized. We also wrapped the call to initializeRox() in a static AtomicBoolean in case the condition is instantiated in more than one place.
Second, we're implementing the getMatchOutcome() method in SpringBootCondition. We've hard-coded it to return false for now. This SpringBootCondition class will be loaded when it is referenced in a @Conditional annotation. This class isn't complete yet, but it's good enough for this first step.
We'll point our application at it so Spring loads it and initializes the API. Our previous revision uses the @ConditionalOnProperty to read Spring properties and decide which handler to load. We need to change to an annotation that can look at code instead of configuration. @Conditional does this. A new instance of CloudBees Feature ManagementCondition is created for each annotation that refers to it, which is why we set up the AtomicBoolean to ensure that initialization is only done once.
Selecting a Spring Controller From a Feature Toggle
Replace the @ConditionalOnProperty annotation on getGreetingHandler() and move the @Primary bean annotation to this method. On getStandardGreetingHandler(), replace @ConditionalOnProperty with @ConditionalOnMissingbean. If the HolidayGreetingHandler is not created, we'll fall through to the standard implementation.
@SpringBootApplication public class Application { @Bean @Primary @Conditional(RolloutCondition.class) public GreetingHandler getGreetingHandler() { return new HolidayGreetingHandler(); } @Bean @ConditionalOnMissingBean(name = "HolidayGreetingHandler") public GreetingHandler getStandardGreetingHandler() { return new StandardGreetingHandler(); } public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
Back on the CloudBees Feature Management website, click Next for the prompt to build and run your application.
Build and run it, and after a few moments, you'll see this message:
Implementing a Managed Spring Feature Toggle
With the return value of getMatchOutcome() set to false, we see the standard greeting when we run our app. Now, let's turn that return value into a managed feature flag. If we look at our CloudBees Feature Management dashboard and check flags, we see this:
There are no flags, and there is no way to create one. That's because we create them from our application, not from the dashboard. In Java, managed flags are RoxFlags. These flags are members of a RoxContainer, which is registered with the CloudBees Feature Management API. Let's make a few changes to CloudBees Feature ManagementCondition. First, add a variable declaration toward the top and an implements qualification to the class definition.
@Component public class RolloutCondition extends SpringBootCondition implements RoxContainer { private static AtomicBoolean initialized = new AtomicBoolean(false); private static Log log = LogFactory.getLog(RolloutCondition.class); public static RoxFlag holidaySeason = new RoxFlag(); ....
Next, add a call to Rox.register() to iniitalizeRox():
private void initializeRox() { CountDownLatch roxFirstFetch = new CountDownLatch(1); try { Rox.register("Flags", this); ...
Finally, change matchOutcome() to use the feature toggle.
@Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { return new ConditionOutcome(holidaySeason.isEnabled(), ""); }
RoxContainer is an interface with no methods. By having CloudBees Feature ManagementCondition implement it, we can pass it to Rox.register() inside initializeRox(). The call to register() accepts a String that acts as a namespace. We used Flags. Then, we added a single RoxFlag member named holidaySeason. Managed flags must be public, so CloudBees Feature Management can see them. Last, we modified getMatchOutcome() to return the result of holidaySeason.isEnabled(), which is a boolean value. Run the application again and then look for your feature toggle in the CloudBees Feature Management dashboard.
Flags.holidaySeason is there! We're ready to start using our flag from the management console.
Managing a Feature Flag
We manage flags by adding them to experiments. An experiment is a scheme for controlling flags (and other variables) in production. Click on Production in the left-hand side menu and then click Experiments. This will bring up a screen with a Create Experiment button. Click that and then fill out the new experiment window appropriately.
Select Set Audience.
And we see a console for setting flags to true, false, or split. If we run our tests now, we see that holidaySeason is false.
$ curl -i http://localhost:8080/greeting HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8 Content-Length: 13 Date: Tue, 30 Aug 2022 22:05:05 GMT Hello, World!
Let's change it to true and restart the service.
When we run our tests again, the results are different!
$ curl -i http://localhost:8080/greeting HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8 Content-Length: 22 Date: Tue, 30 Aug 2022 22:25:48 GMT Happy Holidays, World!
We can change the behavior of our application without touching code or configuration files. Before we wrap up, let's take a look at the experiment on the console again. Flip the flag from true to split.
We don't just have the ability to change the application behavior from the console; we can also experiment (hence the name) with how often the application loads the different greeting handler. This is the power of feature flags.
What Else Can Feature Flags Do?
This guide demonstrates how to get started with CloudBees Feature Management in a Java project. CloudBees Feature Management's documentation has details on how you can do a great deal more with flags, experiments, and groupings. You now have an understanding of feature flag management and how it can improve your Java code. You've seen how it can help you manage projects and eliminate unnecessary deployments. Get to it!