Practical guide: Feature Toggles with Spring Boot and Unleash

Practical guide: Feature Toggles with Spring Boot and Unleash

In this practical guide, I’ll demonstrate how to use Feature Toggles for different use case scenarios. You’ll learn how to configure your project to use an out-of-the-box feature toggle router: Unleash.

Table of Contents

What are feature toggles?

Feature toggles, also referred to as feature flags, are ‘switches’ in your application that control whether you execute a piece of code or not, or you run an alternative code block.

Don’t worry if this definition is not enough for you; we’ll go into practical examples that will make you understand this concept better.

Why should you use feature toggles?

Let’s analyze some major benefits of using feature flagging.

Development efficiency

Many IT companies work with big projects in which many developers need to contribute at the same time. These projects usually have multiple active branches that, after review, are merged to the main branch (which goes to production). In big projects, this leads frequently to merge conflicts, caused by multiple people working on the same codebase in parallel.

With feature toggles, you can save most of the wasted time in solving merge conflicts. Everybody can work directly on the main branch, reducing the possibility of running into conflicts. How is this possible without breaking the logic? The developers put the code under development behind a disabled feature flag, and this code will not be executed until the feature flag is enabled.

Another characteristic of feature flagging that helps increase development efficiency is that developers feel safe when they release new changes. They can use a canary release approach to verify if the new feature is working fine before enabling it for all the users in production. We’ll see this in practice in this article.

Minimize risks

If you have a software product used by thousands of users, each time you release a new feature you are assuming risks:

  • You may have missed some test cases, and the new functionality gets broken in production. You may even break existing functionality.
  • Despite having validated the change you’re introducing with some users, it might turn out that most of your user base doesn’t like the new changes.
  • The performance of your system degrades. A common reason is the lack of representative data to run performance tests in a staging environment, so you don’t know the impact on performance until you reach the production system.

With feature toggling you can minimize or even eliminate all these risks. You can turn on the new functionality only for some users, and then measure the performance impact, check if it’s working fine or not, and ask for feedback. After that, you can choose to enable it for everybody or to keep enabling it for parts of your userbase until you reach the total (for better control of the performance impact for example). We’ll put this into practice.

Optimize conversion rate

Sometimes, you might be in doubt between two alternative approaches for the same functionality. In the simplest case, imagine you aren’t sure if your new button will attract more users to buy a subscription if you make it red and uppercase, or orange and lowercase. In more complex scenarios, you could even have two different user flows that you want to test against real users to see which one gives a better conversion (the user registered an account, bought your product, or reached a certain state in your system that you consider a success).

You can run these tests in advance with some selected users in a fake environment. However, that’s expensive, time-consuming, and might not be very representative unless you do it with many users.

Feature flags allow you to segment your users in buckets and present different options to these different groups. Then, you can use your analytics to measure which group performs better according to your goals, for example, conversion rate. This is a very powerful strategy.

The practical case

To demonstrate some benefits of feature toggling, I’ll use a near-real-life example application: a Dad Jokes API server. The main functionality of this application is to produce dad jokes and expose them via a REST API. There is no database in this app, so the jokes are hard-coded in the repository layer.

The application is built on top of Spring Boot and Java 16. To create the skeleton project, you can use Spring Initializr. If you want to replicate the configuration I use in this project, use this start.spring.io link.

Figure 1. Creating the skeleton project
The complete source code is also available on Github: Feature Toggling Example. If you like it, give it a star!

Unleash, Feature Toggles as a Service

For this example and any other app in real life, I would never build my Feature Toggle engine from scratch. Instead, I want to use an existing tool that allows me to create, manage, and analyze Feature Toggles.

I’m integrating Unleash, an open-source Feature Toggling service (github.com/Unleash/) that is also available as a hosted service that you can use out of the box - unleash-hosted.com.

Therefore, the first thing we need to do is to add the corresponding dependency to our pom.xml file. The complete pom.xml file is available on GitHub.

    <dependency>
        <groupId>no.finn.unleash</groupId>
        <artifactId>unleash-client-java</artifactId>
        <version>4.1.0</version>
    </dependency>

Listing 1. Adding the Unleash dependency to the project

Release fast with confidence - Unleash

Injecting the Request Context with userId in Spring Boot MVC

To make the example more realistic, we’ll use the user identifier as the context to determine whether a feature toggle should be active or not. This is what you do in many of the use cases: gradual roll-outs to all users, canary-testing with internal users, etc.

Let’s create a RequestContext bean with Request scope, so we use it per request thread to store the user that is performing the API call.

package io.tpd.dadjokes.requestcontext;

import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.RequestScope;

@Component
@RequestScope
public class RequestContext {

    private String userId;

    public void setUserId(final String userId) {
        this.userId = userId;
    }

    public String getUserId() {
        return userId;
    }
}

Listing 2. The RequestContext class

Now, we create a Spring’s HandlerInterceptor to capture all HTTP requests, extract a custom header USER-ID and put it in the RequestContext bean. See Listing 3.

@Component
public class RequestContextInterceptor implements HandlerInterceptor {

    private final RequestContext requestContext;

    public RequestContextInterceptor(RequestContext requestContext) {
        this.requestContext = requestContext;
    }

    @Override
    public boolean preHandle(final HttpServletRequest request,
                             final HttpServletResponse response,
                             final Object handler) {
        requestContext.setUserId(request.getHeader("USER-ID"));
        return true;
    }
}

Listing 3. The HandlerInterceptor implementation to extract a custom header

Then, we need to create a bean implementing WebMvcConfigurer to add this interceptor to our application configuration. That’s what we do in Listing 4.

@Component
public class RequestContextMvcConfigurer implements WebMvcConfigurer {

    private final RequestContextInterceptor requestContextInterceptor;

    public RequestContextMvcConfigurer(final RequestContextInterceptor requestContextInterceptor) {
        this.requestContextInterceptor = requestContextInterceptor;
    }

    @Override
    public void addInterceptors(final InterceptorRegistry registry) {
        registry.addInterceptor(requestContextInterceptor);
    }
}

Listing 4. Using WebMvcConfigurer to add a custom request interceptor

With these classes in place, we can now inject the bean RequestContext in any other Spring bean with scope Request, and we’ll be able to access the user identifier. We’ll use this property to pass it as a context to Unleash, our feature toggle router.

What we get with these three classes is proper separation between the logic used to figure out the user that made the request and the rest of our functionality.

Configuring Unleash in Spring Boot

The Unleash Java Client is very easy to configure following some simple steps detailed in the User Guide.

To separate the configuration from the implementation, we can use the Spring Boot’s application.yml file to store the property values. See Listing 5.

unleash:
  appName: "dad-jokes"
  clientSecret: "overridden-with-env-var"
  apiUrl: "https://app.unleash-hosted.com/bluechair/api/"
  instanceId: "1"

Listing 5. Unleash configuration properties in application.yml

As you can see, I’m not publishing my private client secret key on unleash-hosted.com to the git repository. Instead, I can override this property when running the Spring Boot app (e.g. with an environment variable named UNLEASH_CLIENTSECRET). If you use the hosted version of Unleash, remember to set this environment variable to your client secret before running the application. We’ll see later how to pass this value via the command line.

The next step is to configure the Unleash instance and put it in the application context. This way, we can benefit from dependency injection. See Listing 6.

@Configuration
public class UnleashConfiguration {

    @Bean
    public UnleashConfig unleashConfig(
            @Value("${unleash.appName}") String appName,
            @Value("${unleash.instanceId}") String instanceId,
            @Value("${unleash.apiUrl}") String apiUrl,
            @Value("${unleash.clientSecret}") String clientSecret,
            UnleashContextProvider unleashContextProvider) {
        return UnleashConfig.builder()
                .appName(appName)
                .instanceId(instanceId)
                .unleashAPI(apiUrl)
                .unleashContextProvider(unleashContextProvider)
                .customHttpHeader("Authorization", clientSecret)
                .build();
    }

    @Bean
    public Unleash unleash(UnleashConfig unleashConfig) {
        return new DefaultUnleash(unleashConfig);
    }
}

Listing 6. Unleash initialization in a Spring Boot @Configuration class

Apart from the basic configuration as it’s detailed in Unleash docs, I’m using a UnleashContextProvider class. We didn’t create that class yet. This provider will connect our previous implementation (which extracts the user identifier) to Unleash. See Listing 7 for the implementation of CustomUnleashContextProvider.

@Component
@RequestScope
public class CustomUnleashContextProvider implements UnleashContextProvider {

    private final RequestContext requestContext;

    public CustomUnleashContextProvider(final RequestContext requestContext) {
        this.requestContext = requestContext;
    }

    @Override
    public UnleashContext getContext() {
        return UnleashContext.builder()
                .userId(requestContext.getUserId())
                .build();
    }
}

Listing 7. A custom UnleashContextProvider to pass the user id from the request

The provider has also a Request scope since the context is changing per request. It uses the RequestContext via dependency injection and passes its userId property to the object that Unleash uses to store the Feature Toggling context: UnleashContext. You can pass here any other context properties you may need, such as the environment where the application is running. You can even append your own properties, so you can later use them in the Unleash feature toggle configuration (through the UI).

Let’s do a quick recap. The first three classes in the requestcontext package (Listings 2, 3, and 4) are responsible for intercepting every request, extracting a header with name USER-ID and putting its value into a bean with scope request: RequestContext. The classes in the unleash package (Listings 6 and 7) configure Unleash, our feature toggle router, to automatically pass the User’s identifier to the context. With this setup in place, we can now use Unleash in its simplest form to determine if a given flag or toggle is enabled for the specific user (which is derived from the request). As mentioned in their docs, this is as easy as:

if (unleash.isEnabled("myFeature")) {
    // logic for requests/users with feature flag enabled
} else {
    // logic for requests/users with feature flag disabled
}

Listing 8. Feature flag toggle branches with Unleash

We won’t entangle our logic with feature-flag-specific code. Instead, we’ll use an abstraction class to make sure we can test our logic properly and keep the feature toggle router isolated. You’ll see how in the coming sections.

Dad Jokes’ domain and functionality

After doing some initial configuration, we need to build the Dad Jokes app model, API, and data store.

Let’s leverage the Java records for the Dad Joke class and its response object, which we’ll return from the controller. See Listings 9 and 10.

package io.tpd.dadjokes.jokes;

import com.fasterxml.jackson.annotation.JsonProperty;

public record DadJoke(@JsonProperty("joke") String joke) {
}

Listing 9. The DadJoke record

package io.tpd.dadjokes.jokes;

import java.util.List;

import com.fasterxml.jackson.annotation.JsonProperty;

public record DadJokesResponse(@JsonProperty("jokes") List<DadJoke> dadJokes) {
}

Listing 10. The DadJokeResponse wrapper

See Listing 11 for the repository interface intended to retrieve a list of Dad Jokes.

package io.tpd.dadjokes.jokes;

import java.util.List;

public interface DadJokesRepository {
    List<DadJoke> getDadJokes();
}

Listing 11. The DadJokesRepository interface

Its implementation is an in-memory list of predefined dad jokes. We could also add a real database, but that wouldn’t be relevant for our goal.

package io.tpd.dadjokes.jokes;

import java.util.List;

import org.springframework.stereotype.Repository;

@Repository
public class DadJokesRepositoryMemImpl implements DadJokesRepository {

    private List<DadJoke> IN_MEMORY_JOKES = List.of(
            new DadJoke("""
                    I only know 25 letters of the alphabet.
                    I don't know y."""),
            new DadJoke("""
                    What happens when you put your hand in a blender?
                    You get a hand shake.
                    """),
            new DadJoke("""
                    If you think swimming with dolphins is expensive, you should
                     try swimming with sharks.
                    It cost me an arm and a leg!""")
    );

    @Override
    public List<DadJoke> getDadJokes() {
        return IN_MEMORY_JOKES;
    }
}

Listing 12. The DadJokesRepository implementation

Given that this application is quite simple, we will skip the service layer of the classic Controller-Service-Repository layered design. We’ll inject the repository in the controller layer, where we have a GET endpoint to fetch the list of jokes from the /jokes endpoint.

package io.tpd.dadjokes.jokes;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/jokes")
public class DadJokeController {

    private final DadJokesRepository dadJokesRepository;

    public DadJokeController(final DadJokesRepository dadJokesRepository) {
        this.dadJokesRepository = dadJokesRepository;
    }

    @GetMapping
    public ResponseEntity getDadJokes() {
        return ResponseEntity.ok(new DadJokesResponse(dadJokesRepository.getDadJokes()));
    }
}

Listing 13. The DadJokeController exposes a REST API endpoint

Running the application from the command line

As you might have noticed when we introduced the repository layer implementation (Listing 12), we’re using the Java Text Blocks with triple quotes, and Java records. This means you have to use Java 16, or enable the preview features in a previous version (e.g. JDK 15).

We can run the app using the included Maven wrapper and the spring-boot:run goal in Maven. Remember that you need to pass your own Unleash client secret key as in Listing 15.

$ ./mvnw spring-boot:run -Dspring-boot.run.arguments="--unleash.clientSecret=your-client-secret"

Listing 15. Running the app from command line

Once the app has started, we can make http requests to localhost:8080/jokes and get the predefined list. To be consistent in all the examples used in this post, I’m already including the header USER-ID, but our logic still doesn’t use the value in the controller or anywhere else. In Listing 16, I use HTTPie because it’s multi-platform, but you can also use curl or your favorite HTTP client. You can download HTTPie from this link.

$ http :8080/jokes USER-ID:1
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/json
Date: Mon, 15 Mar 2021 06:56:55 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
    "jokes": [
        {
            "joke": "I only know 25 letters of the alphabet.\nI don't know y."
        },
        {
            "joke": "What happens when you put your hand in a blender?\nYou get a hand shake.\n"
        },
        {
            "joke": "If you think swimming with dolphins is expensive, you should\n try swimming with sharks.\nIt cost me an arm and a leg!"
        }
    ]
}

Listing 16. Retrieving the list of jokes from the application

This application is a perfect baseline for trying out multiple scenarios where feature flags would be beneficial. Let’s jump into that.

Use cases that benefit from feature flags

We have an application that returns a list of Dad Jokes in JSON format when any given user, identified by a header in the request, calls the endpoint GET [server:host]/jokes.

Let’s analyze how the basic development of this feature and its evolution could use feature toggles to improve the development process and minimize risks, among other advantages.

Develop directly on the production branch (Release toggles)

To give it a practical start, we already have in place the implementation of the first functionality: the Dad Jokes API. However, let’s imagine this is a big project with many people working on it in parallel. Could we add this feature directly on production to save us from dealing with future merge conflicts? Yes, we can leverage feature toggles for that.

Let’s visit for the first time the Unleash UI to configure our first toggle. In my case, I’m using the hosted version, so I can navigate to the Features section within my https://app.unleash-hosted.com space.

Figure 2. The Unleash User's Interface

The first toggle we’ll create will be used to enable or disable the complete feature while we’re developing it. Let’s give it the name ‘DadJokesFunctionalityToggle’. See Figure 3.

Figure 3. Creating a feature toggle in Unleash

We make it of type “Release” because it’s our intention: we want to enable it only when the functionality is ready. For now, we can skip the “Activation Strategies” configuration because it’ll be a basic switch, which is the default strategy. When we enable it, we’ll do it for all the requests no matter the context (the user).

To integrate this feature toggle in our code, we could inject the Unleash bean we created in UnleashConfiguration into the controller and use it directly from there. However, that would couple our business logic to the feature toggle router. We can approach this in a better way, with the creation of an additional abstraction layer: the Application configuration class. See Listing 17. This way, our configuration may come from Unleash, a configuration file, or environment variables.

package io.tpd.dadjokes.jokes;

import org.springframework.stereotype.Component;

import no.finn.unleash.Unleash;

@Component
public class DadJokesAppConfiguration {

    private final Unleash unleash;

    public DadJokesAppConfiguration(final Unleash unleash) {
        this.unleash = unleash;
    }

    public boolean dadJokesFunctionalityEnabled() {
        return unleash.isEnabled("DadJokesFunctionalityToggle");
    }

}

Listing 17. The App configuration class that abstracts the feature toggle router

By making this a Spring component, we can inject it and use its booleans to condition our logic. In this case, we start with a dadJokesFunctionalityEnabled method that checks the Unleash feature toggle status.

We use the app configuration in the controller to switch between exposing the functionality or return an HTTP status code 404 NOT FOUND. See Listing 18. Note that, by doing this, we’re hiding completely the endpoint to users since they just get the same status code as they would get when they hit a random path.

The complete source code is also available on Github: Feature Toggling Example. If you like it, give it a star!
@RestController
@RequestMapping("/jokes")
public class DadJokeController {

    private final DadJokesRepository dadJokesRepository;
    private final DadJokesAppConfiguration appConfiguration;

    public DadJokeController(final DadJokesRepository dadJokesRepository,
                             final DadJokesAppConfiguration appConfiguration) {
        this.dadJokesRepository = dadJokesRepository;
        this.appConfiguration = appConfiguration;
    }

    @GetMapping
    public ResponseEntity getDadJokes() {
        if (appConfiguration.dadJokesFunctionalityEnabled()) {
            return ResponseEntity.ok(new DadJokesResponse(dadJokesRepository.getDadJokes()));
        } else {
            return ResponseEntity.notFound().build();
        }
    }
}

Listing 18. Using a Release Feature Toggle in the controller

If we run the application again, it’ll connect to the Unleash server instance and check the status of the feature toggle. If we created it enabled (as per the default), we’ll still get the dad jokes when we hit the endpoint (as in Listing 16). However, if we disable it via the UI (as shown in Figure 4), we’ll notice soon how we get a 404 response when we try to reach the endpoint. Check Listing 19.

Figure 4. Disabling the Feature Toggle
$ http :8080/jokes USER-ID:1
HTTP/1.1 404
Connection: keep-alive
Content-Length: 0
Date: Mon, 15 Mar 2021 07:30:43 GMT
Keep-Alive: timeout=60

Listing 19. Getting a 404 status when the feature toggle is disabled

Keep in mind that, to keep this practical guide as clearly organized as possible, we first implemented the controller and the repository, and then the feature toggle. The amazing part of this story is that you can do it the other way around. You can first create the endpoint as a placeholder, using a Release Feature Toggle that is disabled from the start. Then, you can continue your development on the main branch without causing any errors. If we would follow that approach, our controller’s code could look as shown in Listing 20.

@GetMapping
public ResponseEntity getDadJokes() {
    if (appConfiguration.dadJokesFunctionalityEnabled()) {
        // TODO john doe is working on this feature
        throw new UnsupportedOperationException();
    } else {
        return ResponseEntity.notFound().build();
    }
}

Listing 20. Trunk development with release toggles

In that case, the feature toggle is the first thing we need. After disabling it, we could create the complete stack of functionality without exposing anything. At the end of the process, we can switch the toggle on and start accepting requests on the endpoint. We’ll put this approach into practice in the section “Gradual Roll-Outs with backward compatibility”.

This is a great achievement because we can improve the development efficiency, especially in organizations with big projects, by giving tools to people to develop on the main branch. However, we can still make it better. We can also improve the release risk if we enable the feature gradually, instead of applying a “big bang” when we switch the toggle on. Let’s cover that use case.

Every commit to production - Unleash

Canary releases (Release toggles)

Canary Releasing is a technique to roll out features on production first to a small subset of users, before making it available to the entire userbase. It minimizes risks since you can verify if everything is working as you expect before extending the functionality to everybody, provided that you have proper observability in your system (e.g. metrics, real-time analytics, good logging, etc.). You can read more about Canary Releasing concepts in Martin Fowler’s Blog.

To make it practical, imagine that our users with identifiers 1 and 2 are the test users. In an evolved version of this app, we could also use a separate header to identify those, but this approach is good enough for the sake of this guide. We feel confident about the implementation of the Dad Jokes feature, so we’re planning to switch it on. However, we want to give it a try on production with our test users first.

In this scenario, instead of the standard Activation Strategy for our Release Feature Toggle, we’ll use a strategy based on user identifiers. Let’s edit our feature toggle and add a new Activation Strategy, ‘withUserId’. Then, we add the values for which this feature is going to be enabled, 1 and 2, the user ids. Note that Unleash accepts any sort of string as a user identifier, so this works for emails, UUIDs, and other different identifiers you may want to use.

Figure 5. Using a withUserId activation strategy

Now, when we enable the feature toggle, it’ll apply only to the users who match this strategy. In our case, users with identifier 1 or 2. Let’s test this from the console with users 1 and 4. See Listing 21.

$ http :8080/jokes USER-ID:1
HTTP/1.1 200
[...]
{
    "jokes": [
        {
            "joke": "I only know 25 letters of the alphabet.\nI don't know y."
        },
        {
            "joke": "What happens when you put your hand in a blender?\nYou get a hand shake.\n"
        },
        {
            "joke": "If you think swimming with dolphins is expensive, you should\n try swimming with sharks.\nIt cost me an arm and a leg!"
        }
    ]
}

$ http :8080/jokes USER-ID:4
HTTP/1.1 404
[...]

Listing 21. Validating the ‘withUserId’ activation strategy

Great, now the functionality is only visible for our test users. For the rest of the users, the feature is invisible since they get a 404 status code. If we had a web client or mobile app, we could use this same feature flag on the frontend side to identify whether the API endpoint should be even called or not, so we would never expect to return a 404 in practice.

Gradual Roll-outs

Once we give the new functionality a try with the test users in our Canary Release, we might want to roll this feature out to everybody. As a first alternative, we know already that we could simply change back the activation strategy to the standard one and enable the toggle.

However, we can leverage Unleash and feature toggling here to make the release go gradually. This approach gives us even more safety because we can also validate other aspects such as performance, or validate flows that we can’t exercise with our test users nor with standard tests.

In Unleash, we can do this by adding a new Activation Strategy to our feature toggle: gradualRolloutUserId. This strategy allows us to set the desired percentage of users for which we want to enable the feature, and it uses stickiness to ensure that the same user always gets the same feature status over time. See Figure 6.

If we keep the previous strategy active as well, we’ll make sure our test users still have access to the functionality at all times.

Figure 6. Setting up a Gradual Roll-out activation strategy

With this configuration, and without the need to change anything in our code, we get a major benefit. Now, we can gradually adjust the percentage over a day, a week, or an hour (as we wish), and let more users access the feature as we validate aspects like performance or errors in the system.

To put this into practice, run some requests with at least 10 or 20 different IDs. You’ll see how users 1 and 2 still have access to the feature, and some other users too, depending on the percentage you chose. Increase the threshold in steps and you’ll see how users who didn’t have access in previous requests, now they get to see the jokes.

At some point, you’ll reach 100% of users, so you can consider the feature as successfully released. Then, we could clean up our code and the feature toggle configuration to remove this switch. However, in this case, we can also decide to keep it as a Kill Switch. You’ll see how in the next section.

Release fast with confidence - Unleash

Kill Switches or Maintenance Mode toggles

An additional use case for Feature Toggles is the introduction of Kill Switches, also known as Ops Toggles. In this scenario, we use a feature toggle to disable part of the system without impacting the rest of the functionalities.

This possibility is very useful in cases where you work with an architecture that allows for partial degradation or partial failures. If you detect an incident or a major bug in your invoicing module, you can disable it temporarily without bringing the whole system down. Moreover, you can use to perform complex migrations that are simpler to do if the corresponding component is down. Then, you can put this component in maintenance mode.

We can apply this pattern to our Dad Jokes feature as well. We already have that functionality, so we could simply keep it as it is now: an enabled feature toggle, and adjusted to 100% users. However, we can do some small tweaks to make the approach simpler and more appropriate to this use case.

First, we can simplify again the feature toggle configuration in Unleash and keep only the default strategy. Besides, we can change the type from ‘Release’ to ‘Kill Switch’. The feature toggle type doesn’t impact the functionality but helps us keep everything organized. See Figure 7.

Figure 7. Making our toggle a Kill Switch

There’s something we can improve also in the code. Right now, we return a 404 NOT FOUND HTTP status when the user hits the disabled feature. With a Kill Switch, returning a 503 SERVICE UNAVAILABLE is much more appropriate. Our frontend, or an API client in general, can easily identify that the service is not available at the moment and render an adequate message. See Listing 22.

The complete source code is also available on Github: Feature Toggling Example. If you like it, give it a star!
@GetMapping
public ResponseEntity getDadJokes() {
    if (appConfiguration.dadJokesFunctionalityEnabled()) {
        return ResponseEntity.ok(new DadJokesResponse(dadJokesRepository.getDadJokes()));
    } else {
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build();
    }
}

Listing 22. Returning a 503 status code from the controller

This setup allows us to switch off for certain periods the Dad Jokes functionality completely, without needing to bring the whole service down. This specific example is not very illustrative, but imagine a service where you expose multiple functionalities. You can filter all Kill Switches in Unleash and use it as an Ops center to disable parts of the system whenever is needed.

Gradual Roll-Outs with Backward compatibility

Creating a new version of your domain, or changing the APIs or their model are very common tasks in an evolving product or service. Many organizations struggle with this type of release because you should keep backward compatibility unless you can bring your server and clients down for a while to make the rollout. Even with that possibility, it’s still a big risk if something goes wrong.

Let’s navigate through a practical example of how we can approach this with feature toggles. In a newer version of our functionality, we want to monetize our service. As a result of the design, we come up with a new model for the DadJoke record class with a new premium indicator that we’ll use in the future. See Listing 23. For the sake of backward compatibility, we create this model in a new DadJokeV2 record.

public record DadJokeV2(@JsonProperty("joke") String joke,
                        @JsonProperty("premium") boolean premium) {
}

Listing 23. The DadJokeV2 record

We’ll also need a new response wrapper. See Listing 24.

public record DadJokesResponseV2(@JsonProperty("jokes") List<DadJokeV2> dadJokes) {
}

Listing 24. The DadJokesResponseV2 record

The Repository layer can be already evolved without keeping both versions because we can do the mapping at a different level. We’ll see how very soon. The new interface and its implementation are shown in Listings 25 and 26.

public interface DadJokesRepository {
    List<DadJokeV2> getDadJokes(boolean includePremium);
}

Listing 25. DadJokesRepository uses V2 and a boolean for filtering

package io.tpd.dadjokes.jokes;

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.stereotype.Repository;

@Repository
public class DadJokesRepositoryMemImpl implements DadJokesRepository {

    private List<DadJokeV2> IN_MEMORY_JOKES = List.of(
            new DadJokeV2("""
                    I only know 25 letters of the alphabet.
                    I don't know y.""", true),
            new DadJokeV2("""
                    What happens when you put your hand in a blender?
                    You get a hand shake.
                    """, false),
            new DadJokeV2("""
                    If you think swimming with dolphins is expensive, you should
                     try swimming with sharks.
                    It cost me an arm and a leg!""", true)
    );

    @Override
    public List<DadJokeV2> getDadJokes(boolean includePremium) {
        return includePremium ? IN_MEMORY_JOKES :
                IN_MEMORY_JOKES.stream().filter(j -> !j.premium())
                        .collect(Collectors.toList());
    }
}

Listing 26. The repository implementation with premium jokes

As you can see, we classified some of our jokes as premium, and give the possibility to the caller to include premium jokes or to filter them out.

Since we’re planning to introduce a new feature toggle, we need a new method in our configuration abstraction. See Listing 27. This method controls to what users we’re going to deliver the new API model, using a new feature toggle DadJokesModelV2Toggle (not created yet in Unleash).

@Component
public class DadJokesAppConfiguration {
    // ...
    public boolean dadJokesV2Enabled() {
        return unleash.isEnabled("DadJokesModelV2Toggle");
    }

}

Listing 27. Adding a new toggle to the configuration layer

In the controller, we can now use this condition to determine whether we should return the new response or the old one, that we can construct from the new structure. See Listing 28.

@GetMapping
public ResponseEntity getDadJokes() {
    if (appConfiguration.dadJokesFunctionalityEnabled()) {
        var dadJokes = dadJokesRepository.getDadJokes(true);
        return appConfiguration.dadJokesV2Enabled() ?
                ResponseEntity.ok(dadJokes) :
                ResponseEntity.ok(mapToV1(dadJokes));
    } else {
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build();
    }
}

private List<DadJoke> mapToV1(final List<DadJokeV2> dadJokes) {
    return dadJokes.stream().map(j2 -> new DadJoke(j2.joke()))
            .collect(Collectors.toList());
}

Listing 28. Using a feature toggle for backward compatibility

We call dadJokesV2Enabled() to check if we should return the new API objects or the previous ones. The method mapToV1 converts the V2 jokes to plain DadJoke objects (without the premium field). This ensures backward compatibility with ‘old’ clients.

For now, we’re using a hardcoded value of true in the getDadJokes method to give access to everybody to all the jokes. We’ll extend this idea in the next section about Permission Toggles.

In Unleash, we simply add this feature and follow the same Canary Release and Gradual Rollout phases as we did earlier. First, we enable this for test users. Then, we gradually roll out to everybody.

You might be wondering why you should go this way instead of using API versioning. With the latter, you could open a new endpoint /v2/jokes and let the client determine what endpoint to call. That’s a good strategy too. However, if you’re in control of both the server and the client (imagine there is a Dad Jokes website), you can use the feature toggle on both sides to gradually move everybody from the V1 API to the V2 API while you verify everything works fine, validate the usability, etc. You don’t need multiple releases for that, or run an emergency rollback if something goes wrong.

As another alternative to our approach, you could also keep both V1 and V2 API endpoints in the backend and use the feature toggle in the frontend to decide which one to call. In this sense, you still use feature toggling but only on the frontend side. The only drawback is that you need to start using API versioning anyways, which always introduces complexity in your API design.

To put our code into practice, we need to create a new Feature Toggle in Unleash. See Figure 8.

Figure 8. Gradual Roll Out for Model evolution

If we make some requests with our test users, you’ll see how they get the new API model. Other users’ functionality is not impacted. See Listing 29.

$ http :8080/jokes USER-ID:1
HTTP/1.1 200
...
[
    {
        "joke": "I only know 25 letters of the alphabet.\nI don't know y.",
        "premium": true
    },
    {
        "joke": "What happens when you put your hand in a blender?\nYou get a hand shake.\n",
        "premium": false
    },
    {
        "joke": "If you think swimming with dolphins is expensive, you should\n try swimming with sharks.\nIt cost me an arm and a leg!",
        "premium": true
    }
]

$ http :8080/jokes USER-ID:3
HTTP/1.1 200
...
[
    {
        "joke": "I only know 25 letters of the alphabet.\nI don't know y."
    },
    {
        "joke": "What happens when you put your hand in a blender?\nYou get a hand shake.\n"
    },
    {
        "joke": "If you think swimming with dolphins is expensive, you should\n try swimming with sharks.\nIt cost me an arm and a leg!"
    }
]

Listing 29. Only test users get the new API model

With the gradual rollout strategy, you can move the slider until you get to the 100%. Then, you’ll return only the new model for all the users. As mentioned earlier, this is a nice alternative to API versioning if you want to be in control of the transition to the new model on production.

Every commit to production - Unleash

Permissions

We can also use Feature Toggles to control which users have access to certain features, although this is not the best use case for feature toggles in my opinion. Let’s go first through the practical example to better understand how it works.

Since we have now premium jokes, we can split users between those who have access to these premium jokes and those who don’t. To start with, we create a new configuration flag to abstract a new feature toggle. See Listing 30.

The complete source code is also available on Github: Feature Toggling Example. If you like it, give it a star!
@Component
public class DadJokesAppConfiguration {
    //...
    public boolean premiumJokesPermission() {
        return unleash.isEnabled("PremiumJokesToggle");
    }
}

Listing 30. Configuration method for the new permission toggle

Then, in the controller, we can use this method instead of the hardcoded boolean we used in the previous section. See Listing 31.

@GetMapping
public ResponseEntity getDadJokes() {
    if (appConfiguration.dadJokesFunctionalityEnabled()) {
        var dadJokes = dadJokesRepository.getDadJokes(
                appConfiguration.premiumJokesPermission()
        );
        return appConfiguration.dadJokesV2Enabled() ?
                ResponseEntity.ok(dadJokes) :
                ResponseEntity.ok(mapToV1(dadJokes));
    } else {
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build();
    }
}

Listing 31. Introducing a permission toggle in the controller layer

In Unleash, we create the Feature Toggle with type Permission as in Figure 9. Apart from giving access to our test users 1 and 2, we also included in the list the users 8 and 10, because they pay a substantial amount for having access to this entertainment service.

Figure 9. A Permission Toggle in Unleash

After this configuration has been finished, we can restart our application and give it a try. As an example, user 8 has access to the premium jokes whereas user 9 doesn’t. See Listing 32. If more users pay, we can just add them to the ‘WithUserId’ strategy conditions. We could even use an integration from the payment gateway to Unleash’s administration API to add these users programmatically.

$ http :8080/jokes USER-ID:8
HTTP/1.1 200
...
[
    {
        "joke": "I only know 25 letters of the alphabet.\nI don't know y.",
        "premium": true
    },
    {
        "joke": "What happens when you put your hand in a blender?\nYou get a hand shake.\n",
        "premium": false
    },
    {
        "joke": "If you think swimming with dolphins is expensive, you should\n try swimming with sharks.\nIt cost me an arm and a leg!",
        "premium": true
    }
]

$ http :8080/jokes USER-ID:9
HTTP/1.1 200
...
[
    {
        "joke": "What happens when you put your hand in a blender?\nYou get a hand shake.\n",
        "premium": false
    }
]

Listing 32. Using a permission toggle to control user access to the features

Even though this toggle is very useful, this is not how a more realistic application usually works. In a more elaborated Dad Jokes app, you would ask the user to log in using a web interface, for example. That’s the Authentication aspect of your system. If you want to control permissions for users to access certain functionalities, you can use Authorization on top of your user management and put roles in place. When a user logs in, you attach to this user entity the roles that are associated with it, for example, has_premium_access. Then, in your logic, you can check these roles to determine access. This alternative approach shares many of the same principles, with just the difference that the access control lives in a different component and not in your Feature Toggle Router. Some providers offer Authentication and Authorization as-a-service, such as Auth0 or AWS.

In any case, this approach with Permission Toggles can be good for simple use cases, MVP versions of your project, or if you want to have everything centralized in the same place. As usual, you should balance all pros and cons and make your decision based on your specific requirements and constraints.

Experiments, A/B testing, or Multi-variant testing

If you want to try multiple variants of the same feature to see which one performs better (e.g. regarding conversion), you can also use feature toggles and segment users across the variants.

In the simplest case, an A/B experiment (two variants), you can create a feature toggle that enables an experiment for 10% of your users and define two variants. Then, you can later analyze which group is leading to better results.

I’ll leave to you the practical implementation of this use case as an exercise. The requirement is simple: we want to try dad jokes that are all-uppercase and all-lowercase, and see which version the users like more. You can assume there is a mechanism to get feedback from the user, or you can implement your own if you want (for example a different endpoint to ‘like’ a dad joke).

Here are some hints:

  • Unleash allows you to create Variants for a feature toggle. You could, for example, create the variant names uppercase and lowercase.
  • In your code, you can retrieve the variant for any user with unleash.getVariant("ToggleName").getName().
  • If you go for the full version and you implement the ‘like’ endpoint, you’ll need to add an id to each joke and store (e.g. in memory) the variant of the user who liked the joke. Later, you can use another endpoint to retrieve the stats (or just log them to the console).

Multi-variant experiments are very useful to make the right choices when you’re evolving your product or service. Once you start using them, it’s hard to go back because they bring a lot of value to different areas like marketing, user experience, etc.

Cleaning up unused feature toggles

Having multiple features in your code under the control of feature toggles is a powerful tool but can make your code quite complicated to test and maintain. Therefore, you must clean up all the feature toggles that you don’t need any longer.

In our application, we decided to transform our first feature toggle into a Kill Switch, so we kept it there with a different categorization. However, we also used a feature toggle to move our users to a V2 model, and we didn’t clean it yet.

To avoid creating a mess, we must find time to clean up our logic after all users have been migrated to this version. You can see this in practice in a separate Pull Request I created in the GitHub repo. Don’t forget to do this when you work with feature toggles. A good practice for achieving this is to create a task in advance for cleaning up the toggle, and make sure it’s planned in your backlog.

About Unleash

As you saw in this guide, a feature toggle router as a service can significantly simplify the work you need to start using feature toggles. Unleash has a great advantage: it’s an open-source project, so you can manage the service yourself if you choose to. Furthermore, you can contribute to making the project better.

On the other hand, its hosted service can save you time in configuration and maintenance. You can request a trial of one month and decide if it fits your requirements.

Figure 10. Unleash - Feature Toggle list view
Figure 11. Unleash - Feature Toggle metrics

In this guide we used Java but Unleash has multiple client SDKs, so you can use the service from a wide variety of programming languages.

Conclusions

Feature toggles (or flags) can bring you multiple advantages in different scenarios: faster releases, risk control, experimentation, ways of working, etc.

In this guide, we used a practical example and we modified it to introduce Release Toggles, Kill-Switches, and Permission Toggles. We also explained how to run a multi-variant experiment. The GitHub project uses Java and Spring Boot, but the same principles apply to any other programming language and framework.

The project structure has proper abstractions to keep the feature toggle router isolated, and to benefit from dependency injection as much as possible. Feel free to use it as a template for your future projects.

I hope you found this practical guide useful and with enough use cases to give you a good understanding of the concepts. If that’s the case, you can show your appreciation by starring the Github repository. Thanks! If you have questions or want to say thank you, you can also leave a comment.

To continue reading about extra concepts of feature toggles and their classification, read this great article in Martin Fowler’s Blog.

This post is sponsored by Unleash.

Moisés Macero's Picture

About Moisés Macero

Software Developer, Architect, and Author.
Are you interested in my workshops?

Málaga, Spain https://thepracticaldeveloper.com

Comments