Feature flags offer a solution to decouple the deployment of a feature from its release. This can help us continuously deploy new changes without releasing features immediately to the public. LaunchDarkly is one of the major players in the feature flag ecosystem and provides SDKs for almost every use case and programming language. This article will cover local development and testing hints for using LaunchDarkly for a Java project.
This article assumes you have a basic understanding of LaunchDarkly and the concept of feature flags. It's not a beginner-friendly introduction to feature toggling or LanchDarkly. Consult the LaunchDarkly documentation to get started.
LaunchDarkly Java Maven Project Setup
Integrating LaunchDarkly to a Java project takes two things: a valid LaunchDarkly access key and a new dependency for our project:
1 2 3 4 5 | <dependency> <groupId>com.launchdarkly</groupId> <artifactId>launchdarkly-java-server-sdk</artifactId> <version>5.7.1</version> </dependency> |
To make things simple, let's assume we're using the following abstraction for our LaunchDarkly feature flag setup:
1 2 3 4 5 | public interface FeatureFlagClient { String getCurrentValue(String featureFlagKey, String username); void registerChangeListener(String featureFlagKey, String username, FeatureFlagValueChangeHandler changeHandler); } |
The functionality is minimalistic and only allows to query for the string value of a feature flag. We're using an interface to abstract any LaunchDarkly internals from the users of our feature flag mechanism.
We provide the actual LaunchDarkly feature flag client by implementing the FeatureFlagClient
interface:
1 2 3 4 5 6 7 8 9 10 | public class LaunchDarklyFeatureFlagClient implements FeatureFlagClient { public final LDClient ldClient; public LaunchDarklyFeatureFlagClient(LDClient ldClient) { this.ldClient = ldClient; } // ... } |
This way we hide any LaunchDarkly internals and can potentially switch the underlying feature flag implementation without much effort.
For demonstration purposes, let's stay framework-independent and introduce a good old Java factory that instantiates an actual FeatureFlagClient
:
1 2 3 4 5 6 7 | public class FeatureFlagClientFactory { public static FeatureFlagClient buildOnlineClient(String accessKey) { LDClient onlineClient = new LDClient(accessKey); return new LaunchDarklyFeatureFlagClient(onlineClient); } } |
On production, we require an online LDClient
that talks to the LaunchDarkly API. Therefore we pass the secret access key.
When using frameworks like Spring, Quarkus, Micronaut, or Jakarta EE, we'd rather go for a dependency injection approach and let the container create a singleton instance for our FeatureFlagClient
.
For local development and testing purposes, the requirements for the LDClient
are quite different. We may not want any real HTTP communication going on between our application and the LaunchDarkly servers.
Therefore, let's see what alternatives we have.
Local Java Development and LaunchDarkly
When starting our application locally a real LDClient
may not be the best option.
First, we would have to share the SDK key in a secure manner. That shouldn't be a big problem if we introduce a dedicated local environment on LaunchDarkly, as the access key would differ from production.
However, there's still a potential downside as our team members develop features in parallel. Hence, two team members may override their feature flag state constantly.
Furthermore, connecting to LaunchDarkly locally results in additional API calls. If we're short on our API budget, this is another reason against an online version of the LDClient
for local development.
Still, we want our developers to be able to test their feature toggled implementations locally.
As a solution, we can create a version of the LDClient
that is backed by a local file. This file includes the state of all feature flags and can be changed while the application is running. This way, there's no need to interact with the LaunchDarkly web interface, as we can toggle features by simply modifying a local file.
We enrich our FeatureFlagClientFactory
with a new method that creates such an offline file-based LDClient
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public static FeatureFlagClient buildOfflineFileBasedClient(String inMemoryFileLocation) { Path localFeatureFlagStateFilePath = Paths.get(inMemoryFileLocation); if (Files.exists(localFeatureFlagStateFilePath)) { LDClient fileBaseClient = new LDClient( "invalid-ignored-access-key", new LDConfig.Builder() .dataSource( FileData.dataSource().filePaths(localFeatureFlagStateFilePath).autoUpdate(true)) .events(Components.noEvents()) .build()); return new FeatureFlagClient(fileBaseClient); } else { return buildOfflineClient(); } } |
The access key is ignored as there won't be any communication with the LaunchDarkly API. We pass the location of the file(s) (which can be multiple) using the dataSource
method. With autoUpdate(true)
we instruct the client to listen to file changes and adjust the feature flag values accordingly.
We can choose between a simple (without targeting rules, the values apply to everyone) and a complex (with targeting rules) JSON/YAML file for the local feature flag file.
For demonstration purposes, let's take the simple route and store the following file at the root of our project:
1 2 3 4 5 6 7 8 | { "flagValues": { "root-log-level": "DEBUG", "vat-amount": 19.5, "max-parallel-sessions": 32, "enable-event-publishing": true } } |
What's left is to adjust our component setup/bean wiring to use this version of the FeatureFlagClient
when working locally.
1 2 | FeatureFlagClient client = FeatureFlagClientFactory .buildOfflineFileBasedClient("./simple-in-memory-flags.json"); |
Every developer can now test their changes in an isolated environment and toggle on or off features on demand.
Local LaunchDarkly Development Alternatives
As an alternative to this file-based approach, we can create an offline client that always returns the default value:
1 2 3 4 | public static FeatureFlagClient buildOfflineClient() { LDClient offlineClient = new LDClient("ignored-access-key", new LDConfig.Builder().offline(true).build()); return new LaunchDarklyFeatureFlagClient(offlineClient); } |
We can use this as a backup if all other approaches don't fit our use case.
The benefit of these two LDClient
variants is that they don't require any active network connection. No calls are made over the network. This allows us even to develop our application on planes or a German train.
LaunchDarkly Use Case: Change the Root Log Level
Let's implement a feature flag use case to transition to our next topic: testing with LaunchDarkly.
We're implementing a generic feature flag use case that's valid for many applications: Changing the root log level during runtime. While this is more a configuration and less a feature flag in the sense of experiments and A/B testing, it's still a good usage of LaunchDarkly to get started with feature flags.
When using Log4j2, the required code to change the root log level of our application is little:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public class RootLogLevelUpdater { private static final Logger LOG = LoggerFactory.getLogger(RootLogLevelUpdater.class); private final FeatureFlagClient featureFlagClient; public RootLogLevelUpdater(FeatureFlagClient featureFlagClient) { this.featureFlagClient = featureFlagClient; } public void registerListener() { LOG.info("Going to register root log level updater"); featureFlagClient.registerChangeListener( "root-log-level", "duke", (oldValue, newValue) -> { LOG.info("Going to change the root log level from '{}' to '{}'", oldValue, newValue); Configurator.setRootLevel(Level.valueOf(newValue)); }); } } |
Please note that this implementation only changes the root log level. Specific loggers that define the log level for a package or class are untouched. We'll only see log level changes for statements related to the root logger.
Depending on the application framework we're using, there may be built-in mechanisms for this, but this technique comes in handy for older or plain Java SE applications.
We're registering a feature flag change listener for this feature flag evaluation. Our LaunchDarkly client does the following for this under the hood:
1 2 3 4 5 6 | @Override public void registerChangeListener(String featureFlagKey, String username, FeatureFlagValueChangeHandler changeHandler) { ldClient .getFlagTracker() .addFlagValueChangeListener(featureFlagKey, new LDUser.Builder(username).build(), (changeEvent) -> changeHandler.handle(changeEvent.getOldValue().stringValue(), changeEvent.getNewValue().stringValue())); } |
Again, this is a really basic implementation for demonstration purposes.
What's left is to create the feature flag root-log-level
inside the LaunchDarkly interface, configure the valid values (e.g. TRACE
, DEBUG
, ERROR
), and define a default log level to then activate the feature flag.
With this listener in place, how are we now going to test it?
Java Unit Testing With LaunchDarkly
When it comes to testing our Java code that depends on a feature flag evaluation, we need a similar solution compared to local development. We do not want to initialize a real LaunchDarkly client when running our test suite locally or on our CI server (e.g., GitHub Actions).
Let's assume we want to write a unit test for the following Java class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class OrderService { private final FeatureFlagClient featureFlagClient; public OrderService(FeatureFlagClient featureFlagClient) { this.featureFlagClient = featureFlagClient; } public void processOrder(String orderId) { if("plane".equals(featureFlagClient.getCurrentValue("primary-shipment-method", "duke"))) { // distribute via plane }else { // different implementation } } } |
Our OrderService
depends on the FeatueFlagClient
and evaluates the primary-shipment-method
feature flag when processing orders. For the corresponding unit test of this method, we want to verify the behavior of our class for at least two cases: the primary shipment method is plane
and is not plane
.
When instantiating our class under test (OrderService
) we don't want to depend on a real implementation of the FeatueFlagClient
and hence use Mockito to mock it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @ExtendWith(MockitoExtension.class) class OrderServiceTest { @Mock private FeatureFlagClient featureFlagClient; @InjectMocks private OrderService orderService; @Test void shouldDeliverViaPlaneWhenConfigured() { when(featureFlagClient.getCurrentValue(anyString(), anyString())) .thenReturn("plane"); orderService.processOrder("42"); // further verification } } |
Mocking the feature flag client allows us to control its behavior during the test execution fully. This way, we can make it return any feature flag value we need for a particular test.
One additional benefit of using our FeatureFlagClient
abstraction over directly depending on the LDClient
is that we don't need the Mockito inline-mock-maker
. The LDClient
is a final
class, and we would need to tune Mockito to mock this class if we would directly depend on it. That's no big deal per se.
See the static mocking or constructor mocking with Mockito for an example of how to activate this mock maker.
Java Integration Testing Setup With LaunchDarkly
Coming back to our root log level change listener, we need a different solution to test this class. It gets quite complicated (if not impossible) to write a test for this class if we solely depend on Mockito and try to fake the behavior of LaunchDarkly.
Furthermore, when writing integration or end-to-end tests, we might want to have more fine-grained control over the current feature flag values. We may even want to change their value during a test to verify a feature flag value listener fires correctly.
We can use a file-based LaunchDarkly client as showcased for the local development section. But there's an even better alternative: Implementing a LaunchDarkly client backed by an in-memory dataset:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | public class TestDataFeatureFlagClient implements FeatureFlagClient { private final TestData testData; private final LDClient ldClient; public TestDataFeatureFlagClient() { this.testData = TestData.dataSource(); this.ldClient = new LDClient( "ignored-access-key", new LDConfig.Builder().dataSource(testData).events(Components.noEvents()).build()); } @Override public String getCurrentValue(String featureFlagKey, String username) { return ldClient.stringVariation(featureFlagKey, new LDUser.Builder(username).build(), "unknown"); } @Override public void registerChangeListener(String featureFlagKey, String username, FeatureFlagValueChangeHandler changeHandler) { ldClient .getFlagTracker() .addFlagValueChangeListener(featureFlagKey, new LDUser.Builder(username).build(), (changeEvent) -> changeHandler.handle(changeEvent.getOldValue().stringValue(), changeEvent.getNewValue().stringValue())); } public void updateFeatureFlag(String featureFlag, String newValue) { this.testData.update(this.testData.flag(featureFlag).valueForAllUsers(LDValue.of(newValue))); } } |
As we abstract the underlying feature flag implementation with our FeatureFlagClient
interface, we can provide a new implementation of it inside src/test/java
. Hence it will be only accessible for our tests, and we can't accidentally use it in production.
We still use the underlying LDClient
from LaunchDarkly but use a TestData
instance as the data source. The TestData
class is part of the LaunchDarkly Java SDK and is an in-memory representation of our feature flag state.
We can easily modify the current feature flag value by mutating the state of the TestData
instance. Users of this test feature flag client use the updateFeatureFlag
method to change the feature flags at their convenience.
Java LaunchDarkly Integration Test Example
Let's use this new TestDataFeatureFlagClient
to test our RootLogLevelUpdater
:
1 2 3 4 5 6 7 8 9 10 11 12 13 | class RootLogLevelUpdaterTest { private TestDataFeatureFlagClient testDataFeatureFlagClient; private RootLogLevelUpdater cut; @BeforeEach void setUp() { this.testDataFeatureFlagClient = new TestDataFeatureFlagClient(); this.cut = new RootLogLevelUpdater(testDataFeatureFlagClient); } // ... upcoming test } |
As our class under test depends on an instance of the FeatureFlagClient
interface, we can simply swap the implementation for testing purposes. We do so by passing an instance of the TestDataFeatureFlagClient
class.
Given the fact that we now have full control over the feature flag value and can change it on-demand during the test execution, let's implement a test to verify our log level change:
1 2 3 4 5 6 7 8 9 10 11 12 | @Test void shouldUpdateRootLogLevel() { assertNotEquals(Level.TRACE, LogManager.getRootLogger().getLevel()); cut.registerListener(); testDataFeatureFlagClient.updateFeatureFlag("root-log-level", "TRACE"); await() .atMost(2, TimeUnit.SECONDS) .untilAsserted(() -> assertEquals(Level.TRACE, LogManager.getRootLogger().getLevel())); } |
We first assert that we don't start with a TRACE
log level to ensure we actually change the level. Next, we call our class under test and register the feature flag change listener.
What's left is to trigger the listener by changing the root log level to trace. What follows is a verification using Awaitility (part of the Testing Toolbox). As this feature flag listener is an asynchronous operation, we can't immediately assert that the root log level has changed. We give the test two seconds to detect the change.
Summary: LaunchDarkly Java Testing and Development Hints
With this article, we saw various recipes to provide a convenient use of LaunchDarkly for local Java development and testing. The technique we used for testing the feature flag listener can be used to test the application as a whole. When using Spring Boot, for example, we could override the FeatureFlagClient bean for testing purposes and use a bean of type TestDataFeatureFlagClient
.
The in-memory TestData
approach for writing integration tests could also work for local development. However, using a file-based approach provides a more straightforward way of interacting with the current feature flag state.
Overall, LaunchDarkly is an excellent tool to master feature flags. If you need a solution to get started with feature flags as a concept and don't want to invest some $$$, take a look at the open-source alternative Togglz.
You can find additional content around feature toggling on Tom's reflectoring.io blog:
- Feature Flags with Spring Boot
- Zero Downtime Database Changes with Feature Flags – Step by Step
- Feature Flags in Java with Togglz and LaunchDarkly
The source code for this blog post is available on GitHub.
Joyful testing,
Philip