Background
Feature flags or feature toggles are a way to control releasing software allowing for code to be continually deployed into production environments while optionally controlling whether the functional change the code enables is accessible.
This is especially true in microservice environments where often feature releases may need to be coordinated amongst multiple service deployments over a period of time.
In OSDU a new feature or ADR may state that it requires a feature flag to release safely. This may be because of the risk involved in the change, the length of time needed to deliver the feature, or the feature is experimental and wants to be trialed before being formally adopted.
For example policy service in OSDU used feature flags to control its release. This was because of both the length of time needed to deliver this feature spanned multiple releases, the impact was to multiple services and the risk of failure was high as it fundamentally changed the authorization controls of the system.
In this document we will look at the different implementation options that are available for CSPs for using feature flags in the OSDU.
Usage
The java core common library in OSDU provides an interface and 2 implementations that can be chosen to be injected at runtime. This uses the strategy pattern where each CSP can choose which IFeatureFlag implementation they want to use at runtime.
Each CSP can decide which strategy to use at runtime independently of other CSPs, allowing the strategy that best suits your needs..
The IFeatureFlag interface looks like this
public interface IFeatureFlag {
boolean isEnabled(string featureName);
}
When a service needs to control the release of a feature it can inject an IFeatureFlag implementation to check whether the specified feature is enabled.
@Autowire
IFeatureFlag featureFlag;
public void myMethod() {
if(featureFlag.isEnabled("featureName")) {
//new functionality you want to control releasing
}
}
Where "featureName"
should be a constant value in all places the feature check is needed. By default a feature flag controlled by application properties is used, however other implementations can be used as well as custom ones created.
Below we review these options.
Enabling features using App Properties
This is the default behavior that is injected whenever IFeatureFlag is declared unless otherwise specified.
With this approach the "featureName" is a property within the application.properties resource file of the service. For example a property may be added in the application properties that matches
feature.policy=true
This declares a property called feature.policy and sets it to true. Then whenever there is code like the following
@Autowire
IFeatureFlag featureFlag;
....
....
public void myMethod() {
if(featureFlag.isEnabled("feature.policy")) {
//new functionality
}
}
It will attempt to retrieve the "feature.policy"
property value from the application properties resource. Only if it is set to true will the new code be enabled. If the property is missing from the application properties it defaults to false meaning by default the feature is not enabled.
pros
- simple to use and implement
- its obvious from the code which features are enabled
cons
- the scope of impact is large, all uses of a deployment are impacted when a feature is enabled making it harder to test new features on smaller groups of people
- to change a feature flag requires at least a rebuild and deployment. This is slow to enable/disable features when they are needed.
- it is harder to coordinate enabling a feature across multiple services as each service needs a rebuild and deployment to enable a feature with the same property repeated across service application property files.
Enabling features using Partition service properties
We can declare different IFeatureFlag strategies to be injected at runtime. For instance we can declare the Partition Feature Flag be used at runtime instead of the application properties based one by declaring the preferred strategy in your application properties resource file.
featureFlag.strategy="dataPartition"
This strategy retrieves the feature from the partition service. This allows for the property to be pulled dynamically at runtime the same as any other property in the partition service.
If we had the same code as shown in the previous example that checked for the feature flag
@Autowire
IFeatureFlag featureFlag;
....
....
public void myMethod() {
if(featureFlag.isEnabled("feature.policy")) {
//new functionality
}
}
This would look for a key/value pair being returned from the partition service with a key that matched "feature.policy"`and a value set to true
. The partition ID sent on the request is used by this strategy to check if the feature flag has been set. This allows for features to be enabled/disabled for specific partitions.
By default this implementation will return false if the property is missing or not found in the partition service.
pros
- The scope of impact is less as only users within a partition are impacted not an entire deployment. This allows for scenarios like enabling only on development partitions or less used partitions for example.
- The flag can be set dynamically at runtime. This can be quicker to turn on and off when required.
- It can be used to more easily coordinate feature enablement across services as all services pull the same flag value from the same partition at runtime.
cons
- requires access to the partition service to set a feature flag. This is more difficult as only the system can normally access this service at runtime.
- its less observable which features have been enabled as it requires access to the database or partition service APIs.
Adding custom IFeatureFlag implementations
If you want to add your own feature flag implementation you need to do 3 things
- Provide an implementation of IFeatureFlag
- Create a new named bean
- Declare its use.
For example below we have a very basic implementation of IFeatureFlag that always returns true if the feature name matches the expected value.
public class MyFeatureFlag implements IFeatureFlag {
public MyFeatureFlag(string expectedFeatureName) {
this.expectedFeatureName = expectedFeatureName;
}
private string expectedFeatureName;
public boolean isEnabled(string featureName) {
return this.expectedFeatureName.equals(featureName);
}
}
This is just for demonstration purposes as this implementation is not very useful but shows the intent that a new implementation can do whatever it wants as long as it inherits from the interface.
Secondly we will create a new bean that declares conditional usage under the prefix 'featureFlag' and name 'strategy'. This declares that this IFeatureFlag implementation will be injected whenever the conditional property is declared with 'myFeatureFlag'. Its implementation creates and injects any depdencies needed by the implementation.
@Configuration
public class MyFeatureFlagConfig {
@Bean
@ConditionalOnProperty(prefix = "featureFlag", name = "strategy", havingValue = "myFeatureFlag")
public IFeatureFlag create() {
return new MyFeatureFlag("myFeaturesName");
}
}
Finally in the application properties file we need to either add or replace the existing declaration to say we want to use our new implementation.
featureFlag.strategy="myFeatureFlag"