Feature flags

F

Empty app shells are quite boring. To be valuable an app needs to provide features to the user.

In my architecture these features are delivered by capabilities as web components and composed into the app using a static site generator.

Web components however do not 'just work', they need to be defined as custom elements before their tags will be interpretted by the browser.

At first I thought this would be inconvenient, but in reality this aspect makes them perfect for use as feature flags.

The term Feature Toggles (aka Feature Flags) was defined by Martin Fowler as a way to decide which features are enabled and which one aren't. They can be used for the purpose of canary releases, A/B testing, load reduction, and much more.

I added some basic feature activation logic to the infrastructure shell which makes use of webcomponents as a feature toggle mechanism.

Feature Activation

Feature activation lifecycle

The feature activation logic runs when the page is loaded and consists of the following steps:

  1. Micro app initialization
  2. Feature toggles
  3. Feature activation
  4. Custom element definition

Let's break these down!

Micro app initialization

Each capability defines one or more micro apps.

Each micro app must register itself in the apps array of the shell instance.

The micro app is responsible to register any feature contained in the capability into the shells features array when the initialization step occurs.

import shell from "./dish.shell.js";
import { CoordinationFeature } from "./clubmanagement.coordination.feature.js";
import { CoordinationSelectorFeature } from "./clubmanagement.coordination.selector.feature.js";

class CoordinationApp{
    
    constructor(){
        this.name = "Coordination Micro App";
    }

    async initialize(){         
       shell.features.push(new CoordinationFeature());
       shell.features.push(new CoordinationSelectorFeature());
    }

}

var app = new CoordinationApp();

shell.apps.push(app);

export default app;

Features

A feature registered by the app is responsible to define any custom elements for the web components it encapsulates, but only if the feature is active.

import { GroupSelector } from "./coordination/components/group-selector.js";
import { RoleSelector } from "./coordination/components/role-selector.js";
import { SegmentSelector } from "./coordination/components/segment-selector.js";

class CoordinationSelectorFeature{
    
    constructor(){
        this.name = "Coordination Selector Feature"

        this.isActive = false;
    }    

    activate(){    
        if(this.isActive){
            customElements.define('group-selector', GroupSelector);            
            customElements.define('segment-selector', SegmentSelector);
            customElements.define('role-selector', RoleSelector);
        }
    }

}

export { CoordinationSelectorFeature }

Feature flags

Features can decide themselves if they are enabled by default, but to unlock scenarios such as A/B testing, gradual releases, etc... the shell will need to be able to enable or disable features.

Feature toggles can iterate the feature list and activate or deactivate any of them.

import shell from "./dish.shell.js";

class CoordinationSelectorToggle{

	async toggle(features){
        // any condition could be added here, e.g. check a query string parameter for A/B test
		var selectorFeature = features.find(f => f.constructor.name == 'CoordinationSelectorFeature');
		selectorFeature.isActive = true;			
	}

}

shell.featureToggles.push(new CoordinationSelectorToggle());

Putting it all together

So apps, features and flags register themselves into the shell. The sole purpose of the shell is to call them in the correct order.

  • Initialize the apps
  • Toggle the features
  • Activate the features
import { ShellApp } from "./dish.shell.app.js";
import { Topics } from "./dish.shell.pubsub.js";

class Shell{

    constructor(){
        this.apps = [];
        this.features = [];
        this.featureToggles = [];        
        this.initialized = false;
        this.featuresToggled = false;
    }

    async initialize(){
        this.apps.push(new ShellApp());
        
        for(var app of this.apps){
            if (typeof app.initialize === 'function'){
                await app.initialize();
            }            
        }

        this.initialized = true;        
    }

    async toggleFeatures(){
       
        for(var toggle of this.featureToggles){
            if (typeof toggle.toggle === 'function'){
                await toggle.toggle(this.features);
            }            
        }

        this.featuresToggled = true;        
    }

    async activate(){    
        if(!this.initialized){
            await this.initialize();
        }

        if(!this.featuresToggled){
            await this.toggleFeatures();
        }
       
        for(var feature of this.features){
            if (typeof feature.activate === 'function'){
                await feature.activate();
            }
        }

    }
}

export default new Shell()

And of course the shell needs to be activated when the page is loaded, you can do this by calling activate at the end of the html document.

import shell from "./dish.shell.js"
(async function(){
  
	await shell.activate();

})();  

Next up

I covered most of the infrastructure code needed to build offline capable PWA's, from now on I'll start focussing on the features themselves. Don't hesitate to reach out to me if you have any questions or would like more details on certain aspects.

About the author

YVES GOELEVEN

I've been a software architect for over 20 years.

My main areas of expertise are large scale distributed systems, progressive web applications, event driven architecture, domain driven design, event sourcing, messaging, and the Microsoft Azure platform.

As I've transitioned into the second half of my career, I made it my personal goal to train the next generation of software architects.

Get in touch

Want to get better at software architecture?

Sign up to my newsletter and get regular advice to improve your architecture skills.

You can unsubscribe at any time by clicking the link in the footer of your emails. I use Mailchimp as my marketing platform. By clicking subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp's privacy practices here.