Click here to Skip to main content
14,545,210 members

Feature Flags (aka Feature Toggles)

Rate this:
5.00 (5 votes)
Please Sign up or sign in to vote.
5.00 (5 votes)
17 Apr 2020CPOL
Separation of deployment and feature releases: how to enable and disable features in applications (services) at configuration time
In modern (agile) (micro)service oriented application landscapes, you want to deliver features as soon as possible. Preferably, through an automated deployment pipeline. However, you want to separate releasing features from deployments, and you want to have control when and to whom to release functionality (or, when and to whom to revoke functionality). Putting features behind a flag that can be enabled and disabled for groups can help achieve this goal.

Sample Image - maximum width is 600 pixels

Using feature flags to drive your product’s release strategy and continuous delivery

Introduction

This article provides the concept of Feature Flags, aka Feature Toggles; after that, a possible design follows with pros and cons; then, two possible implementations follow; one in Java (11), one in C# (7).

Last but not least, a wrapup and conclusion is given with some (I trust) good advice.

In short, Feature Flagging allows to modify system behavior by configuration, without changing code. So, in your production application environment, functionality ("features") can be switched on and off for certain (groups of) users without (re-, rollback-) deployment of binaries. This concept is a pattern of the type "Separation of concern", or "Single responsibility". After all, releasing binaries (deployment) and releasing functionality are separated from each other. In general, it is a good idea not to combine two responsibilities in one action (or component). By applying feature flagging, a new release with new features can be deployed, without releasing per-se the new features to everyone. And even so, it is possible to revoke features for certain groups or individuals, without having to rollback a binary release (deployment of a previous version).

See related articles:

  1. Roland Roos, Aspect-Oriented Programming and More Patterns in Micro-Services
  2. Martin Fowler, feature-toggles
  3. Stack Flow What-is-a-feature-flag
  4. Other possible C# implementation example

How is this article built up?

  • Examples in Java and C# of final usage
  • Concept of Feature Flagging/Toggling
  • High level design and rationale (pros and cons of the design)
  • Java implementation details
  • C# implementation details
  • Wrap-up and conclusions, and more

Before Reading this Article

Before you start reading this article, a general advice; If you are not familiar with aspect-orientation, it is advised to first read my article on this or the links in there, as the design and implementation in this article relies heavily on aspect-orientation. See [1.] Aspect-Oriented Programming and More Patterns in Micro-Services

Using the Code in Java (11+, Spring boot 2.2+, MVC thymeleaf, Aspectj)

//
// Configuration application.yml example
//
featureFlags:
	feature1:
		enabled : true
		included :
			user1
		
// Java code usage example
// 
// The @before @FeatureFlag aspect does a lookup of Feature flag "feature1" 
// with key being the value of user parameter,
// it is injected in the model and modelandView method parameters,
// available in the GUI (HTML)
// to be used for building up the view:
// For user = "user1"
// modelandView["feature1"] = true

@FeatureFlag(features = {"feature1"}, paramNameForKeyOfFeature = "user")
public void annotatedFeatureOnController(
	String user, 
	Model model, 
	ModelAndView modelandView) {
	
	if (user.compereTo("user1") == 0) {
		assert.isTrue(modelandView["feature1"]);
	}
}
...
//A call to the controller (pseudo code)
Controller.annotatedFeatureOnController("user1", viewModel);

Using the Code in C# (7>, .NET MVC Core 2.1+, Autofac, Caste Core Dynamic Proxy)

//
// Configuration featureFlagConfig.json example
//
"featureFlags": [
	{
		"name": "feature1",
		"enabled": true,
		"included": [
			"user1"
		]
	}
]
		
// C# code usage example
// 
// The [FeatureFlag] dynamic proxy interceptor does a lookup of Feature flag "feature1" 
// with key being the value of user parameter,
// it is injected in the ViewData model,
// available in the GUI (HTML)
// to be used for building up the view:
// For user = "user1"
// ViewData["feature1"] = true

[FeatureFlag("user", new String[] { "feature1" })]
public ActionResult AnnotatedFeatureOnController([FeatureFlagKey] String user)
{
	if (user.compereTo("user1") == 0) 
	{
		assert.isTrue( ViewData["feature1"]);
	}
}
...
//A call to the controller (pseudo code)
HomeController.AnnotatedFeatureOnController("user1");
or
http://locahost:.../Home/AnnotatedFeatureOnController?user="user1"

Concept: The Basic Principles

What Is Feature Flagging?

There are already good articles on this, in general. See also [2.] Martin Fowler, feature-toggles

Remark: Feature toggle and feature flag are synonyms.

A flag in coding-land is commonly known as a boolean 1/0, true/false, on/off. So, the feature is either on or off.

A toggle in coding-land is generally the more dynamic flipping of the flag, 0<->1 , true <-> false, on <-> off. So it refers to flipping the feature from on to off, and vice-versa.

Why Use Feature Flagging?

In modern, micro-service oriented application landscapes that use a "real" agile approach, we would like to deploy and release functionality (features) as early as possible. Preferably, right after it is automatically built, and automatically tested as ok in the build pipeline. However, we don't want this feature to be always released and available to "everyone" once deployed. Wouldn't it be nice to have the functionality deployed to production at once, skipping complex acceptation periods? Wouldn't it be nice if you could just acceptance test it in production, side-by-side with old implementations, without having the feature available? And then, if proven not to interfere with other functionality, to "release" it for early adapters? And finally, if it was proven stable and usable by the early adapters, gradually release it to everyone? Or, if proven to be disruptive, to disable (revoke) it at once without re-deployment or complex rollbacks?
I bet you would say: yes please, but no thanks. "Not possible".
I bet you: it is possible, hands down, I kid you not.
But it comes with a cost and another burden: feature administration.

In this article, we describe a possible, low-impact, relatively simple design and a possible implementation in both Java and C#. In the wrap-up conclusion, we also address some important strategies with this pattern, to prevent a big clutter-up of boolean if-then-else flows in your code.

High Level Design

Sample Image - maximum width is 600 pixels

Looking at this model, we can see the following:

  • We did not use the word "user" in the model. It is called a "Key", which is an abstraction of a "user". That allows for other type of keys then only users to switch features on.
  • It uses Feature aspects to deliver the feature toggles to your code
  • a feature can be either "enabled" (available to everyone), "disabled" (available to no-one) or "filtered" (available to certain users).
  • A "KeyGroup" is a group of "Keys";
  • A feature can have a list of included and/or excluded Keys (aka users) and/or KeyGroups (aka UserGroups).
  • The feature aspects are delivered by a binary library, using a service facade instead of a client-proxy-service facade. As a consequence of that, we have chosen not to remote the feature flag administration. We simply supply a configuration file per service/component, with the needed feature flags for that service/component. If you would need that kind of centralized feature flag functionality, you would need to replace that with a remote feature service and a client concept, with probably some caching and refreshing. This local configuration file per service type of implementation is not very good suited for clustered load-balanced services, like Containers under a Kubernetes load balancer. Indeed, a form of centralization and remoting would be more applicable then.
  • There is no GUI delivered for editing and toggling. Note: In modern (micro-)service oriented landscapes, a scattering of configuration files is not good for either maintenance, nor traceability. To solve that, a very pragmatic but good workable pattern is available (see Config servers in Spring Boot). Just create separate Config Git repositories with all configuration files in there, and treat configuration as "source code". So, checkin/out to your Git Config, with full versioning and traceability. All services can pull their config from there.

So, indeed, as promised, a very KISS (Keep It Stupid and Simple) straight-forward implementation, but quite easily extendable to other needs.

General Implementation Details

We have in both implementations (Java/C#) unit tests to test the code. And a small demo app MVC-ViewModel service with a tag library (in Java, Thymeleaf, in C# Tag Helpers). That is to show how the features "flow" through the chain of control: config file -> model -> service -> controller -> view -> html. And, like any modern setup, we use IOC and a container: Java Spring Boot AppContext with @Autowiring, in C# Autofac attached to the default Microsoft MVC container. I'll write an article on the how and why of IOC with Java Spring Boot and C# Autofac later, so bear with me and skip that if you're not up to speed there. Same holds for unit testing with mocking frameworks (Mockito, Fakeiteasy). An article on how and why of that is coming up as well.

I promised a KISS simple solution in both Java and C#. Here it is in outline.

Model Implementation

Regarding the FeatureFlags model:

  • 4 simple structure classes (FeatureFlags, Feature, KeyGroup, Key)
  • 1 simple data file (.yml in Java, .json in C#)
  • 1 simple service, that holds the FeatureFlags model, and does the Feature lookup in this structure by Feature.name
  • Some initialization code to parse the config data to the class model data structures (Java uses Spring @ConfigurationProperties, C# ConfigurationBuilder with a little bit more setup code)

Regarding the @FeatureFlag("feature1") annotation:

  • Two annotations, @FeatureFlag, @FeatureFlagKey
  • One Aspect: FeatureFlagAspect, that does the lookup of the Feature properties in the model, through the service, and finds the Key value in the Method params, and does the lookup on the Tuple {<keyvalue>, <featurename>}
  • 1 simple service, that does the lookup in this structure: Feature FeatureFlagServiceImpl.getFeature(String forKey, String forFeatureName){..}

Java Implementation Details (Java 11, Spring Boot, Spring AOP, Lombok, Thymeleaf MVC)

Sample Image - maximum width is 600 pixels

Data Structure to Hold Feature Config

Remember, this was the model:

Sample Image - maximum width is 600 pixels

First, implement the configuration in this Spring Boot yaml structure according to this model:

featureFlags:
    feature1:
         enabled : true
         included :
            group1,
            user3
         excluded :
            user4

keyGroups:
    feature1 : 
        keys : 
            user1,
            user2

Second, define a set of classes that match this yaml structure:

@Configuration
@ConfigurationProperties
public class FeatureFlags 
{
    private static final Logger log = LoggerFactory.getLogger(FeatureFlags.class);
    //
    //really does not matter how you call your pop. As long as the method name matched
    //
    private final Map<string, feature=""> featureFlags = new HashMap<>();

    //
    //it does matter how you call this method. get<rootnameinyaml> -> getFeatureFlags
    //
    public Map<string, feature=""> getFeatureFlags() 
    {
        log.debug("getFeatureFlags");
        return featureFlags1;
    }
    private final Map<string, keygroup=""> keygroups = new HashMap<>();

	//
    //it does matter how you call this method. get<rootnameinyaml> -> getKeyGroups
	//
    public Map<string, keygroup=""> getKeyGroups() 
    {
        log.debug("getKeyGroups");
        return keygroups;
    }
    
    @Data
    public static class Feature {
       private String enabled;
       private List<string> included;
       private List<string> excluded;
    }
    
    @Data
    public static class KeyGroup 
    {
       private List<string> keys;
    }
}

Remark 1: The "root" keys in yaml {featureFlags, keyGroups} must match the {getFeatureFlags(), getKeyGroups()} getters.

Remark 2: If you just have a list of Strings to map, use this construction, with line-separated, comma-separated list (Note: Lombok generates with @Data the public getters/setters):

List<String> included;

included :
    group1,
    user3 

Remark 3: Don't forget the @ConfigurationProperties above the class structure.

Remark 4: Just @autowire FeatureFlags as property in a class, that will parse the yaml file to the class structure:

@Service
@ConfigurationProperties
public class FeatureFlagServiceImpl implements FeatureFlagService
{
    private static final Logger log = LoggerFactory.getLogger(FeatureFlagServiceImpl.class);

    @Autowired
	//
	// Because class FeatureFlags has @ConfigurationProperties, 
    // the yaml is mapped to this structure automagically
	//
    private FeatureFlags  featureConfig; 
...
}

This finalizes setting up the data structure parsing.

@FeatureFlag Annotation

I will not go into detail on aspect-orientation, and just give code and some remarks. See for more aspect-oriented details [1.] For this purpose, we use LTW (Load Time Weaving) with only Spring AOP.

First, define the @FeatureFlag annotation:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FeatureFlag {
    //
    //The list of available features
    //
    String[] features() default {""};
    //
    // The nameof the parameter in the annotated method 
    // that is the key-value to lookup the feature from the model
    //
    String paramNameForKeyOfFeature() default "";
}

Note 1: I did not implement the @FeatureFlagKey, but use the FeatureFlag.paramNameForKeyOfFeature as the keyName to lookup in the method;

So, define the @Before @Aspect:

@Aspect
@Component
public class FeatureFlagAspect 
{
    /**
     * Logger to log any info
     */
    private static final Logger log = LoggerFactory.getLogger(FeatureAspect.class);
    
    /**
     * Service delivering the actual Feature for a key/feature name combi
     */
    @Autowired
    private FeatureFlagService  featureConfigService; 
    
     /**
     *  Injects available feature flags into Model and/or ModelAndView of API. 
     * @see unitests for examples 
     *
     *
     * @param joinPoint  place of method annotated
     * @param featureFlag the object with fields of the FeatureFlagKey annotation
     */
    @Before("@annotation(featureFlag)")
    public void featureFlag(
            final JoinPoint joinPoint,
            FeatureFlag featureFlag) 
                throws Throwable 
    {
        String features = Arrays.asList(featureFlag.features())
                .stream()
                .collect(Collectors.joining(","));

        log.info("for features {} of param {}", features, 
                  featureFlag.paramNameForKeyOfFeature());
        Method method = MethodSignature.class.cast(joinPoint.getSignature()).getMethod();
        log.debug("on {}", method.getName());
        Object[] args = joinPoint.getArgs();
        MethodSignature methodSignature = 
            (MethodSignature) joinPoint.getStaticPart().getSignature();
        String[] paramNames = methodSignature.getParameterNames();

        Class[] paramTypes= methodSignature.getParameterTypes();
        String curKey = null;
        Model modelParam = null;
        ModelAndView modelAndViewParam = null;
        for (int argIndex = 0; argIndex < args.length; argIndex++)
        {
            String curParamName = paramNames[argIndex];

            Object curValue = args[argIndex];

            if (curParamName.equals(featureFlag.paramNameForKeyOfFeature()))
            {
                curKey = curValue.toString();
            }
            log.debug("arg {} value {}", curParamName, curValue);

            if (curValue instanceof Model)
            {
                modelParam = (Model)curValue;
            }
            else if (curValue instanceof ModelAndView)
            {
                modelAndViewParam = (ModelAndView)curValue;
            }
        }
        if (curKey != null)
        {
            for(String featureName : featureFlag.features())
            {
                nl.ricta.featureflag.FeatureFlags.Feature curFeature = 
                    featureConfigService.getFeature(curKey.toString(), featureName);
                if (curFeature != null )
                {
                    if (modelParam != null)
                    {
                        modelParam.addAttribute(featureName, curFeature);
                    }
                     if (modelAndViewParam != null)
                    {
                        modelAndViewParam.addObject(featureName, curFeature);
                    }
                }
            }
        }
    }

Note 2: There is logic defined in the FeatureFlagService.getFeature(), that is called from the aspect, that does the lookup in the FeatureFlags model, of the Feature and its properties:

@Service
@ConfigurationProperties
public class FeatureFlagServiceImpl implements FeatureFlagService
{
    private static final Logger log = LoggerFactory.getLogger(FeatureFlagServiceImpl.class);

    private FeatureFlags  featureConfig;  

    @Autowired
    public FeatureFlagServiceImpl(FeatureFlags  featureConfig)
    {
        log.debug("FeatureFlagServiceImpl : #features = {}", 
                   featureConfig.getFeatureFlags().size());
        this.featureConfig = featureConfig;
    }
    
    public Map<string, feature=""> getFeatureFlags() 
    {
        return this.featureConfig.getFeatureFlags();
    }
    
	public Feature getFeature(String forKey, String forFeatureName)
    {
        require(featureConfig != null);
        require(forKey != null);
        require(forFeatureName != null);
        log.debug("getFeature, forkey={}, forFeatureName={}", forKey, forFeatureName);
        Feature lookupFeature = featureConfig.getFeatureFlags().get(forFeatureName);
        if (lookupFeature == null)
        {
            log.debug("forkey={} has no feature {}",forKey, forFeatureName);
            return null;
        }
        if (lookupFeature.getEnabled()=="true")
        {
            log.debug("forkey={} enabled feature for everyone {}",forKey, forFeatureName);
            return lookupFeature;
        }
        else if (lookupFeature.getEnabled()=="false")
        {
            log.debug("forkey={} enable feature for none {}",forKey, forFeatureName);
            return null;
        }
        //else: filtered
        List<string> includedInFeaureList = lookupFeature.getIncluded();
        boolean direct = includedInFeaureList.contains(forKey);
        if (direct)
        {
            log.debug("forkey={} directly has feature {}",forKey, forFeatureName);
            return lookupFeature;
        }
        log.debug("looking up all included keys in keygroups, 
                   to see if key={} is included there...");
        
        for (String included : includedInFeaureList)
        {
            log.debug("lookup group for {}", included);
            KeyGroup lookupGroup = featureConfig.getKeyGroups().get(included);
            if (lookupGroup == null)
            {
                log.debug("forkey={} has no feature {}",forKey, forFeatureName);
            }
            else
            {
                boolean inGroup = lookupGroup.getKeys().contains(forKey);
                if (inGroup)
                {
                    log.debug("forkey={} has feature {}",forKey, forFeatureName);
                    return lookupFeature;
                }
            }
        }
        log.debug("forkey={} has no feature {}",forKey, forFeatureName);
        return null;
    }
}

Now, we are set for the MVC and Controller part:

@FeatureFlag(features = "{feature1}", paramNameForKeyOfFeature = "user")
@GetMapping(value = "/features")
public String featuresPage(Model model, @RequestParam(value = "user") String user) {
	model.addAttribute("featureEntries", featureFlagService.getFeatureFlags().entrySet());
	return "featureflags/features";
}

And, the view html Features.cshtml with Thymeleaf:

...
<div class="main">
<div>
	<h2>Features</h2>
	<div class='rTable' >
		<div class="rTableRow">
			<div class="rTableHead"><strong>Name</strong></div>
			<div class="rTableHead"><span style="font-weight: bold;">Type</span></div>
			<div class="rTableHead"><span style="font-weight: bold;">Included</span></div>
			<div class="rTableHead"><span style="font-weight: bold;">Excluded</span></div>
		</div> 
		<div class='rTableRow' th:each="featureEntry : ${featureEntries}">
			<div class='rTableCell'><span th:text="${featureEntry.getKey()}"></span></div>
			<div class='rTableCell'><span th:text="${featureEntry.getValue().getEnabled()}">
            </span></div>
			<div class='rTableCell'><span th:text="${featureEntry.getValue().getIncluded()}">
            </span></div>
			<div class='rTableCell'><span th:text="${featureEntry.getValue().getExcluded()}">
            </span></div>
		</div>
		<div class="rTableRow">
			<div class="rTableFoot"><strong>Name</strong></div>
			<div class="rTableFoot"><span style="font-weight: bold;">Type</span></div>
			<div class="rTableHead"><span style="font-weight: bold;">Included</span></div>
			<div class="rTableFoot"><span style="font-weight: bold;">Excluded</span></div>
		</div> 
	</div>
</div>
</div>
<div>
	<h2>Features</h2>
	<div class='rTable' >
		<div class="rTableRow">
			<div class="rTableHead"><strong>Name</strong></div>
			<div class="rTableHead"><span style="font-weight: bold;">Type</span></div>
			<div class="rTableHead"><span style="font-weight: bold;">Included</span></div>
			<div class="rTableHead"><span style="font-weight: bold;">Excluded</span></div>
		</div> 
		<div class='rTableRow' th:each="featureEntry : ${featureEntries}">
			<div class='rTableCell'><span th:text="${featureEntry.getKey()}"></span></div>
			<div class='rTableCell'><span th:text="${featureEntry.getValue().getEnabled()}">
            </span></div>
			<div class='rTableCell'><span th:text="${featureEntry.getValue().getIncluded()}">
            </span></div>
			<div class='rTableCell'><span th:text="${featureEntry.getValue().getExcluded()}">
            </span></div>
		</div>
		<div class="rTableRow">
			<div class="rTableFoot"><strong>Name</strong></div>
			<div class="rTableFoot"><span style="font-weight: bold;">Type</span></div>
			<div class="rTableHead"><span style="font-weight: bold;">Included</span></div>
			<div class="rTableFoot"><span style="font-weight: bold;">Excluded</span></div>
		</div> 
	</div>
</div>
</div>
...

C# DotNet Implementation Details (C# 7, .NET Core 2.1 MVC, Razor, AutoFac, Castle Core, AutoFac Dynamic Proxy)

Sample Image - maximum width is 600 pixels

Data Structure to Hold Feature Config

Remember, this was the model:

Sample Image - maximum width is 600 pixels

First, implement the configuration in a json format (featureFlagConfig.json) structure according to this model:

{
	"features": [
		{
			"name": "feature1",
			"type": "enabled",
			"included": [
				"group1",
				"user3"
			],
			"excluded": [
				"user4"
			]
		}
	],
	"keyGroups": [
		{
			"Name": "feature1",
			"keys": [
				"user1",
				"user2"
			]
		}
	]
}

Next, define the class structure model to hold the Feature configuration data:

public enum FeatureType
{
	enabled, 
	disabled,
	filtered
}

public class Feature
{
	public string name { get; set; }
	public FeatureType type { get; set; }
	public List<string> included { get; set; }
	public List<string> excluded { get; set; }
}
public class KeyGroup
{
	public string name { get; set; }
	public List<string> keys { get; set; }
}
public class FeatureFlags
{
	public Dictionary<string, feature=""> features { get; set; }
	public Dictionary<string, keygroup=""> keyGroups { get; set; }
}

Next, to load this configuration json into the model, I used the .NET ConfigurationBuilder:

ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddJsonFile
       ("featureFlagConfig.json", optional: true, reloadOnChange: true);

IConfigurationRoot config = configurationBuilder.Build();

var sectionFeatureFlags = config.GetSection("features");
List<Feature> features = sectionFeatureFlags.Get<List<Feature>>();

var sectionKeyGroups = config.GetSection("keyGroups");
List<KeyGroup> keyGroups = sectionKeyGroups.Get<List<KeyGroup>>();

FeatureFlags featureFlags = new FeatureFlags()
{
	features = features.ToDictionary(f => f.name, f => f),
	keyGroups = keyGroups.ToDictionary(kg => kg.name, kg => kg),
};

We also need a service, that defines the logic on the feature flag model:

public interface FeatureFlagService
{
	FeatureFlags FeatureConfig { get; set; }
	void LoadFeatureFlags(String fileName);
	Feature getFeature(String forKey, String forFeatureName);
	List<Feature> getFeatures(String forKey, List<String> forFeatureNames);
}
public class FeatureFlagServiceImpl : FeatureFlagService
{
	public FeatureFlags FeatureConfig { get; set; }

	/// <summary>
	/// 
	/// </summary>
	/// <param name="fileName"></param>
	public virtual void LoadFeatureFlags(String fileName)
	{
		ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
		configurationBuilder.AddJsonFile(fileName, optional: true, reloadOnChange: true);

		IConfigurationRoot config = configurationBuilder.Build();

		var sectionFeatureFlags = config.GetSection("features");
		List<Feature> features = sectionFeatureFlags.Get<List<Feature>>();

		var sectionKeyGroups = config.GetSection("keyGroups");
		List<KeyGroup> keyGroups = sectionKeyGroups.Get<List<KeyGroup>>();

		this.FeatureConfig = new FeatureFlags()
		{
			features = features.ToDictionary(f => f.name, f => f),
			keyGroups = keyGroups.ToDictionary(kg => kg.name, kg => kg),
		};
	}

	/// <summary>
	/// 
	/// </summary>
	/// <param name="forKey"></param>
	/// <param name="forFeatureNames"></param>
	/// <returns></returns>
	public virtual List<Feature> getFeatures(String forKey, List<String> forFeatureNames)
	{
		var features = new List<Feature>();
		foreach(var featureName in forFeatureNames)
		{
			Feature f = getFeature(forKey, featureName);
			if (f != null)
			{
				features.Add(f);
			}
		}
		return features;
	}
	/// <summary>
	/// 
	/// 
	/// </summary>
	/// <param name="forKey"></param>
	/// <param name="forFeatureName"></param>
	/// <returns></returns>
	public virtual Feature getFeature(String forKey, String forFeatureName)
	{
		Debug.WriteLine($"getFeature, forkey={forKey}, forFeatureName={forFeatureName}");

		FeatureConfig.features.TryGetValue(forFeatureName, out Feature lookupFeature);
		if (lookupFeature == null)
		{
			Debug.WriteLine($"forkey={forKey} has no feature {forFeatureName}");
			return null;
		}
		if (lookupFeature.type == FeatureType.enabled)
		{
			Debug.WriteLine($"forkey={forKey} enabled feature for everyone {forFeatureName}");
			return lookupFeature;
		}
		else if (lookupFeature.type == FeatureType.disabled)
		{
			Debug.WriteLine($"forkey={forKey} enable feature for none {forFeatureName}");
			return null;
		}
		//else: filtered
		List<String> includedInFeaureList = lookupFeature.included;
		Boolean direct = includedInFeaureList.Contains(forKey);
		if (direct)
		{
			Debug.WriteLine($"forkey={forKey} directly has feature {forFeatureName}");
			return lookupFeature;
		}
		Debug.WriteLine($"looking up all included keys in keygroups, 
                        to see if key={forKey} is included there...");

		foreach (String included in includedInFeaureList)
		{
			Debug.WriteLine($"lookup group for {included}");
		   
			FeatureConfig.keyGroups.TryGetValue(included, out KeyGroup lookupGroup);
			if (lookupGroup == null)
			{
				Debug.WriteLine($"forkey={forKey} has no feature {forFeatureName}");
			}
			else
			{
				Boolean inGroup = lookupGroup.keys.Contains(forKey);
				if (inGroup)
				{
					Debug.WriteLine($"forkey={forKey} has feature {forFeatureName}");
					return lookupFeature;
				}
			}
		}
		Debug.WriteLine($"forkey={forKey} has no feature {forFeatureName}");
		return null;
	}
}

That finalizes setting up the data structure, feature flag logic and the parsing in C#. As promised simple, in approximately 50 lines of code.

C# [FeatureFlag] Annotation

We begin (again see [1.] Aspect-Oriented Programming and More Patterns in Micro-Services) with defining the FeatureFlag attributes:

 [AttributeUsage(
   AttributeTargets.Method,
   AllowMultiple = true)]
public class FeatureFlagAttribute : System.Attribute
{
	public FeatureFlagAttribute(String keyName, string[] features)
	{
		Features = features.ToList();
		KeyName = keyName;
	}
	public List<string> Features { get; set; } = new List<string>();
	public String KeyName { get; set; } = "";
}

Then, we define the aspect (interceptor) on this annotation:

public class FeatureFlagIntersceptor : Castle.DynamicProxy.IInterceptor
{
	public FeatureFlagService featureFlagService { get; set; }

	public void Intercept(Castle.DynamicProxy.IInvocation invocation)
	{
		Debug.Print($"1. @Before Method called {invocation.Method.Name}");

		var methodAttributes = invocation.Method.GetCustomAttributes(false);
		FeatureFlagAttribute theFeatureFlag = (FeatureFlagAttribute)methodAttributes.Where
		(a => a.GetType() == typeof(FeatureFlagAttribute)).SingleOrDefault();
	
		if (theFeatureFlag != null)
		{
			var paramNameForKeyOfFeature = theFeatureFlag.ParamNameForKeyOfFeature;
			ParameterInfo[] paramsOfMethod = invocation.Method.GetParameters();
			//
			// Iterate params, to get param position index
			//
			int iParam;
			for (iParam = 0; iParam < paramsOfMethod.Count(); iParam++)
			{
				ParameterInfo p = paramsOfMethod[iParam];
				if (p.Name.CompareTo(paramNameForKeyOfFeature) == 0)
				{
					break;
				}
			}
			//
			// Now, as we know the index, get the key value from the actual method call
			//
			string value = (string)invocation.Arguments[iParam];
			List<feature> features = featureFlagService.getFeatures
                                     (value, theFeatureFlag.Features);
			Debug.Print($"2. FeatureFlagAttribute on method found with name = 
                       {theFeatureFlag.Features}\n");
			foreach(Feature f in features)
			{
				Debug.Print($"3. Feature {f.name} exists and type = {f.type}");
			}
		}
		//
		// This is the actual "implementation method code block".
		// @Before code goes above this call
		//
		invocation.Proceed();
		//
		// Any @After method code goes below here
		//
		Debug.Print($"5. @After method: {invocation.Method.Name}\n");
	}
}

And, since we need the FeatureFlag in a REST controller to supply the feature properties to the Razor ViewModel and HTML GUI, we also need the REST controller solution for aspect-orientation, where attribute and aspect are combined in one ActionFilterAttribute class:

public class FeatureFlagActionAtrribute : 
    Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute
    {
		public List<string> Features { get; set; } = new List<string>();
        public String KeyName { get; set; } = "";
		public override void OnActionExecuting
                (Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext context)
		{
			ControllerActionDescriptor actionDescriptor = 
                   (ControllerActionDescriptor)context.ActionDescriptor;
            Debug.Print($"2. @Before Method called 
               {actionDescriptor.ControllerName}Controller.{actionDescriptor.ActionName}");
            var controllerName = actionDescriptor.ControllerName;
            var actionName = actionDescriptor.ActionName;
            IDictionary<object, object=""> properties = actionDescriptor.Properties;
            ParameterInfo[] paramsOfMethod = actionDescriptor.MethodInfo.GetParameters();
            var fullName = actionDescriptor.DisplayName;

            var paramNameForKeyOfFeature = ParamNameForKeyOfFeature;

            var arguments = context.ActionArguments;
            string value = (string)arguments[paramNameForKeyOfFeature];
            
            using (ILifetimeScope scope = BootStrapper.Container.BeginLifetimeScope())
            {
                var featureFlagService = scope.Resolve<featureflagservice>();
                List<feature> features = featureFlagService.getFeatures(value, Features);
                Debug.Print($"2. 
                    FeatureFlagAttribute on method found with name = {Features}\n");
                var ctrler = (Controller)context.Controller;
                foreach (Feature f in features)
                {
                    Debug.Print($"3. Feature {f.name} exists and type = {f.type}");
                    ctrler.ViewData[f.name] = f;
                }
                ctrler.ViewData["features"] = 
                      featureFlagService.FeatureConfig.features.Values;
            }
            
            base.OnActionExecuting(context);
        }
		public override void OnActionExecuted
               (Microsoft.AspNetCore.Mvc.Filters.ActionExecutedContext context)
        {...}
	}

Now, let's connect it all together, in a real life controller like we started in the article:

[FeatureFlagActionAtrribute("user", new String[] { "feature1" })]
public IActionResult DoSomethingWithFilterAction(String user)
{
	Debug.Assert(ViewData["Features"] != null);
	Debug.Assert(ViewData["feature1"] != null);
	return View("Features");
}

And, the view html Features.cshtml with Razor:

...
<table>
	<tr>
		<th>enabled for user</th>
		<th>type</th>
		<th>included</th>
		<th>excluded</th>
	</tr>
	@foreach (var feature in ViewData["Features"] 
              as IEnumerable<nl.ricta.featureflag.Feature>)
	{
		<tr>
			<td>@feature.name <input type="checkbox" checked=@ViewData[feature.name]></td>
			<td>@feature.type</td>
			<td>@(string.Join(",", @feature.included));</td>
			<td>@(string.Join(",", @feature.excluded));</td>
		</tr>
	}
</table>
...

Note 1: A feature checkbox is "checked" in the view, if the FeatureFlagActionAtrribute feature name for that user value is "checked", so either enabled, or filtered and user value is in included list

Note 2: I use autofac as IOC container. I'll write another article on IOC, and add it to the referenced articles in due time. I enabled autofac module registration and the feature flag loading via the (singleton) FeatureFlagService in Startup:

public void ConfigureServices(IServiceCollection services)
{
	var builder = new ContainerBuilder();
	builder.RegisterModule(new FeatureFlagModule());
	BootStrapper.BuildContainer(builder);
	using (var scope = BootStrapper.Container.BeginLifetimeScope())
	{
		FeatureFlagService featureFlagService = scope.Resolve<featureflagservice>();
		featureFlagService.LoadFeatureFlags("featureFlagConfig.json");
	}
	...
}

Conclusion and Points of Interest

What Did We Learn in this Article?

  • Feature flagging allows for earlier release of functionality
  • Feature flagging separates deployment from releasing functionality
  • Defining and implementing the model is relatively easy, in a simple Feature flag model in a re-usable binary library, in a simple file format (.json, .yaml)
  • Defining and implementing the logic is relatively easy, with a simple Feature flag logic service in a re-usable binary library
  • Using aspect-oriented Feature flagging in the Apps, is basically one or two lines of code and a bit of config data, and we added a FeatureFlag (when all library plumbing is in place)
  • Resulting in a simple ViewModel feature flags where we can react on, within the MVC HTML taglibrarys (Razor, Thymeleaf, ...) in the HTML view.

As mentioned, in a clustered, loadbalanced environment, the above solution is not the best implementation(or, just plain bad and unworkable). A remote central feature flag service would be the way to go then. What needs to be added/changed in the model above? Basically, you need to adjust one piece of implementation: loading the model from that remote service, instead of via a file. That's well within a day of work. And move the model loading to a central feature flag micro-service. Okay, another day or two of work.

Strategies for Feature Flagging: Do's and Dont's

I also promised a pattern or strategy for applying Feature flagging in such a way, that the feature flag administration overhead and code clutter-up does not become too big a burden.

Do's

Actually, there are three patterns/strategies you should all apply:

  1. Only put a feature behind a feature flag if:
    1. some technical stability issues are to be expected when the feature is on
    2. some business and/or usability issues are to be expected when the feature is on
    3. you want (permanently) to dis/allow a subset of users to some feature
  2. Remove feature flags as soon as possible in the code (mostly, when everyone has access and the feature is proven stable and usable)
  3. Remove features from the code that were proven unusable for everyone.

In other words: put a feature only behind a flag if there is a very, very good reason, and then and only then; And remove it as soon as the reason is gone.

Dont's

And one strategy you should not apply:

Put everything behind a flag without good reason and let it stay there forever.

The "better save than sorry flagging" habit is a common mistake all too often made when using feature flagging, which usually comes from several "abusive" reasons:

  • Nobody takes responsibility for decisions of whether a flag is needed or not, resulting in "better save than sorry flagging": developers put all new functionality behind a flag by default: "you never know.."
  • "Quality excuse flagging": Quality of all features and code it so bad, that everything is put behind a flag, so you can always disable it if proven too bad.
  • "Hide bad unusable functionality behind a flag": Features that were proven not usable are not removed from the code, but stay disabled behind a flag for ever, for everyone.

If you apply this very bad strategy, both feature flag administration overhead, and flagging code, will become such a burden and source of agony and pain with issues, bugs and degraded testability and usability, that it will very soon become unworkable.

Conclusion

Feature flagging can be implemented simple, and when used properly, allows for (much) faster and smoother releasing of features.

Please do!

But if feature flagging is abused for purposes it was not meant to be, it can lead to mayhem, serious agony, pain and sorrow. It will lead to degraded code, degraded testability, degraded usability and terrible quality.

Don't do that please?

History

  • 16th April, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Roland Roos ICT Architectures
Architect Roos ICT Architectures
Netherlands Netherlands
Roland is an experienced, hands-on architect on modern (micro-)services orientation.
All modern OO-oriented platforms, C# DotNet, Java Spring Boot, Python and C++ have his interest. He is experienced in all those platforms.
Architectures, patterns, concepts and frameworks are more important than tooling and languages, after all. You should apply a tool, platform or language in a modern micro-service, because it fits best to the problem at hand. Not because you're most familiar with itSmile | :) .

Comments and Discussions

 
QuestionDo not conflate feature flags and authorisation Pin
NeverJustHere18-Apr-20 16:17
MemberNeverJustHere18-Apr-20 16:17 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 17 Apr 2020

Stats

5.6K views
36 downloads
5 bookmarked