A Feature Flag Experiment with Config and CDI
Lately I have been doing some research into Feature Toggle approaches, and how these can be used in Micro Services components developed in Java. These are simple true/false values used to determine if a given application feature (or code path) is enabled.
On looking at a MicroProfile quick start with sample code, the Config feature specification looked interesting for tackling a piece of this functionality.
The aim here is to provide a proof-of-concept implementation that allows for feature toggles that can be scoped down to different user attributes. This should be able to act as a starting point for a more complicated implementation if required. While it turned out that this could not be done solely using the Config feature, there appears to be a viable approach when combining this with Context Dependency Injection (CDI).
In the interests of speed and simplicity, this will use a header in the HTTP request to define the role of the user. If this approach was to be used in a production environment this role assignment would need to be provided by something which is verifiable, such as the JWT token provided.
All the source code for this implementation is part of my microprofile experiments project.
Config Feature
The Config Feature in MicroProfile aims to standardise the approach to loading runtime configuration, accepting that applications require to get their configuration values from a variety of hierarchical sources.
Each ConfigSource has a specified ordinal, which is used to determine the importance of the values taken from the associated ConfigSource. A higher ordinal means that the values taken from this ConfigSource will override values from lower-priority ConfigSources. This allows a configuration to be customized from outside a binary, assuming that external ConfigSource s have higher ordinal values than the ones whose values originate within the release binaries.
This will iterate through all the available configuration sources (from highest ordinal to lowest) until it finds a non-null value. This alone has the potential to drastically simplify any configuration loading code which was previously written to look for system properties, and if they were not found use some default configuration values.
This could potentially be used as the “read” side of our feature toggle implementation. This abstracts out how the configuration is ultimately read, as the calling code only cares about the value.
While the focus of my testing is focused on the most recent release of Payara Micro, this functionality has been part of Payara server since the 4.173 release.
The source of the above quote defines the default configuration sources which are required as part of the specification. Implementations of the specification are free to provide their own additional sources, which Payara does.
In the sample code, the file src/main/resources/META-INF/microprofile-config.properties
defines the key/value configuration value pairs that is used by the lowest ordinal configuration source from the default set, so can be used to define any default configuration values which should be used if they are not found elsewhere. This configuration file becomes part of the deployable WAR file produced as the build artifact of the project.
Custom Configuration Sources
Additional configuration sources can be written if the default set are not enough. These need to implement the interface ConfigSource, then are discovered through the Service Provider Interface (SPI) mechanism through class path resources META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource
.
In my experiment project, an implementation FeatureFlagConfigSource was created to read a JSON array from a file on the class path, but this could have just as easily performed an HTTP call to get this JSON array from an API.
An important factor in any system which is used to load data, configuration or otherwise, is performance. While reading a configuration file once is relatively low impact, if this implementation was loading its data from a remote API it would be expected to cache the loaded data, and periodically refresh the held information.
JCache is not part of the MicroProfile specification, and so not part of the template project which has been used as the basis of this experiment project.
As it is provided as part of Payara Micro, I just needed to add it as a dependency in the POM file.
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
<version>1.0.0</version>
<scope>provided</scope>
</dependency>
As we are unable to use the @Inject
annotation to inject a CacheManager instance into the configuration source implementation, a manual lookup from JNDI is required. The approach to getting a CacheManager instance may vary based on the application server you are deploying to, this is the approach used for Payara.
Context ctx = new InitialContext();
CachingProvider provider = (CachingProvider) ctx.lookup("payara/CachingProvider");
CacheManager manager = (CacheManager) ctx.lookup("payara/CacheManager");
This Cache Manager can then be used to get the configured Cache instance for holding the loaded values.
Configuration Value Types
The default type for all values is String. The framework has limited support built in for converting from a string value to some primitive data types. If the value is required to be converted in to a more complex object type, a custom implementation of Converter needs to be written. Just like with the configuration sources, these are discovered through the SPI mechanism, this time using the class path resources META-INF/services/org.eclipse.microprofile.config.spi.Converter
.
In the case of our feature toggle example, a JSON object is used as our value and is converted to a FeatureFlag object containing the conditions where the toggle should be activated. The custom configuration source reads in a JSON array to make up a map of “name” to the JSON object string for the value.
[
{
"name": "feature.one",
"enabled": true,
"properties": {
"requires-header-role": "admin"
}
},
{
"name": "feature.two",
"enabled": false
}
]
However, due to the way that the hierarchy of configuration sources work we can also define settings in the “microprofile-config.properties” file.
future.feature={"name": "future.feature", "enabled": true}
At this point we are able to have our Feature object loaded from the JSON file and injected into the controller with the below code. Getting this directly from the configuration provider in this fashion does not allow us to do any customisation based on the request context.
@Inject
@ConfigProperty(name = "feature.one")
private Feature featureOne;
It would be possible to take this injected value and pass it, along with whatever contextual information is required, to a “resolver” class to determine the final value. This is a pattern that appears to be used in ff4j.This does however add additional code into each endpoint method, when ideally I want to hide all this in framework code.
Injection
Inside of a ConfigSource implementation itself there appears to be no way to get any information on the context of the request, which in a way makes sense as the framework is standalone and not part of JAX-RS or CDI. Two approaches were tested, using @Context for HttpHeaders and @Inject for other CDI resources, but neither resulted in any dependency injection occurring.
So in order to use contextual information, we need to use our own CDI provider instead of the one provided for Config.
In regular CDI, it is possible to specify methods as taking in an InjectionPoint, which provides details on where the value will ultimately be injected into. In this example implementation, the getAnnotated()
method is used to get the annotations which are set on the injection target. The ConfigProperty
annotation could not be reused for the feature name without triggering the Config feature, so a custom annotation FeatureProperty was created with a very similar definition.
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface FeatureProperty {
/**
* The key of the config property used to look up the configuration value.
*
* @return Name (key) of the config property to inject
*/
@Nonbinding
String name() default "";
}
This annotation holds the name of the feature toggle, which is then used by the ResolvedFeatureFlagProducer class to load the FeatureFlag
definition from the Config feature before being turned in to a boolean for injection based on the available context information.
/* Get the config name annotation value */
FeatureProperty configProperty = injectionPoint.getAnnotated().getAnnotation(FeatureProperty.class);
if (configProperty == null) {
throw new IllegalStateException(
"Failed to find required FeatureProperty annotation");
}
final String featureName = configProperty.name();
/* Load the enabled state and return */
return featureFlagResolver.isFeatureEnabled(featureName);
The logic to resolve the feature into a boolean is delegated to another class which is injected by CDI, FeatureFlagResolver. This has the request context information available, and gets the Config instance through CDI also.
@RequestScoped
public class FeatureFlagResolver {
/**
* The headers associated with the request.
*/
@Context
private HttpHeaders headers;
/**
* The config source
*/
@Inject
private Config config;
/* rest of implementation here */
}
While in initial testing it did not seem to be required to define our custom FeatureProperty
annotation as a Qualifier, it is preferable to include to avoid future ambiguous dependencies.
Conclusion
Using this approach we can use a standard part of the MicroProfile specification, with a bit of custom loading and conversion code, to handle loading our feature toggle definitions from a variety of sources. This includes the possibility to have flags which can be defined by an API call, or can use default settings which are bundled in through configuration with the application (or even system properties or environment variables) if the API does not know about them.
To avoid potential clashes, a production scale implementation of this system would incorporate a naming scheme which is specific to your software. This could additionally be used to limit the scope of the feature toggles that an application loads from a remote source (as a micro service may only be interested in a handful of feature flags).
The sample feature flag resolver is a simple implementation as an example. For production usage, or conversion into a library, this could be modified to include additional flipping strategies or at least the required extension points to allow this to be customisable. This could take a few different forms:
- setting the default strategy
- allowing the “FeatureProperty” annotation to take in an optional class parameter with the resolution strategy to use
- providing a mechanism for the feature definition to determine the resolution strategy
The combination of the Config feature and CDI looks like a workable, and relatively clean, solution to implement a feature toggle client.