Recently, I have found myself more convinced about using feature flags in my Angular projects. I was skeptical at first because the code can become quite a mess and hard to maintain properly. But, I have come to appreciate the pros.
I have collaborated with clients who followed a bi-weekly release schedule that came with some challenges. Tying feature releases to these fixed schedules made it really restrictive. We couldn't always adhere to, for example, the legal department. All of our feature releases happened on the scheduled release. However, with the adoption of feature flags, the flexibility to release on any day became feasible. Moreover, the feature flags enable smoother rollbacks in the face of unforeseen issues.
Another advantage is that feature flags enable the developer to create small pull requests. They can merge slices of a feature without immediately deploying.
What is a feature flag?
A feature flag, also known as a feature toggle or feature switch, is a programming technique that allows developers to turn specific features or functionalities of a software application on or off, usually during runtime.
As an illustration, in this article, we will be discussing the fastLogin
feature flag. The feature enables a streamlined login process, making it into a single step. Without the feature flag, you would have to contact customer support, which gives you a code to log in (bad UX, but you get the point).
Getting started
In this example, we will be using an API to retrieve the feature flags that are active, but you could also use a json
file in the code, up to you. First, let's define the structure of the API response.
// feature-flag.service.ts
type FeatureFlagResponse = {
fastLogin: boolean;
fastRegister: boolean;
fastSettings: boolean;
}
The FeatureFlagResponse
consists of features returned by the API. In this case fastLogin
, fastRegister
and fastSettings
.
Managing active feature flags can become quite cumbersome. It is crucial to sync the types with the backend. So it is clear which feature flags are active.
By using the keys within the FeatureFlagResponse
, we can scope our functions to only have access to the keys in the FeatureFlagResponse
. This narrows down the potential feature flags you could use in the front end. If it doesn't exist in the response, you can't use it.
// feature-flag.service.ts
type _FeatureFlagKeys = keyof FeatureFlagResponse;
It is worth noting that I have opted against using only keyof FeatureFlagResponse
. By transforming the types, the output becomes "fastLogin" | "fastRegister" | "fastSettings"
, providing a more user-friendly experience when inspecting types.
// feature-flag.service.ts
export type FeatureFlagKeys = {
[K in _FeatureFlagKeys]: K;
}[_FeatureFlagKeys]
Now, let's integrate the method to retrieve feature flags and store them into a signal. The signal is being used to verify whether a specific feature is currently enabled or not.
// feature-flag.service.ts
@Injectable({ providedIn: 'root' })
export class FeatureFlagService {
http = inject(HttpClient);
features = signal<Record<string, boolean>>({});
getFeatureFlags(): Observable<FeatureFlagResponse> {
return this.http.get<FeatureFlagResponse>('/api/flags').pipe(tap((features) => this.features.set(features)));
}
getFeature(feature: FeatureFlagKeys): boolean {
return this.features()[feature] ?? false;
}
}
We can leverage the service to invoke the API, but what is the optimal place for this action? A suitable moment is during app initialization, and fortunately, Angular offers a DI token to provide one or more initialization functions.
By creating a custom provider function we can adhere to the new Angular standard by calling provideFeatureFlag()
. The factory is being used to initialize the feature flags.
// feature-flag.provider.ts
function initializeFeatureFlag(): () => Observable<any> {
const featureFlagService = inject(FeatureFlagService);
return () => featureFlagService.getFeatureFlags();
}
export const provideFeatureFlag = () => ({
provide: APP_INITIALIZER,
useFactory: initializeFeatureFlag,
deps: [],
multi: true,
})
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
...
provideHttpClient(),
provideFeatureFlag(),
],
};
The feature flags are now accessible from everywhere in the app. The next consideration is integrating them into component templates. One approach could be injecting the FeatureFlagService
wherever it is required. Alternatively, we could use a structural directive.
// feature-flag.directive.ts
@Directive({ selector: '[featureFlag]', standalone: true })
export class FeatureFlagDirective {
templateRef = inject(TemplateRef);
viewContainer = inject(ViewContainerRef);
featureFlagService = inject(FeatureFlagService);
hasView = signal(false);
@Input() set featureFlag(feature: FeatureFlagKeys) {
if (this.featureFlagService.getFeature(feature) && !this.hasView()) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView.set(true);
} else {
this.viewContainer.clear();
this.hasView.set(false);
}
}
@Input() set featureFlagElse(elseTemplateRef: TemplateRef<any>) {
if (!this.hasView()) {
this.viewContainer.createEmbeddedView(elseTemplateRef);
}
}
}
This directive makes it possible to display content only when a specified feature is enabled. Additionally, it offers the flexibility to include an alternative template for when the feature is not enabled. For example when fastLogin
is enabled we can log in with one click. If it's not enabled, we will have to mail the customer support.
<div *featureFlag="'fastLogin'; else notEnabled">
<button>Login with one click</button>
</div>
<ng-template #notEnabled>
<div>Mail us one support@example.com for your login.</div>
</ng-template>
It is worth mentioning that the FeatureFlagKeys
from earlier ensure that the autocomplete feature exclusively suggests the options typed in the FeatureFlagResponse
such as fastLogin
, fastRegister
and fastSettings
.
Attempting a random string will result in a build error, ensuring we stick to the set feature flags.
This strict typing not only makes the development process smoother but also acts as a strong mechanism for quickly spotting references to feature flags that have been removed, making the cleanup process much easier.
But, what about routes? We could create a route guard that utilizes the service as well.
// feature-flag.guard.ts
export const featureFlagGuard = (feature: FeatureFlagKeys) => {
return () => {
const featureFlagService = inject(FeatureFlagService);
return featureFlagService.getFeature(feature);
};
};
// routes.ts
{
path: 'fast-register',
canMatch: [featureFlagGuard('fastRegister')],
loadComponent: () => ..,
}
Or you could just use it inline.
canMatch: [() => inject(FeatureFlagService).getFeature('fastLogin')]
And there we go. Feature flags in Angular. If you have questions or use cases that we could tackle. Please let me know.
Top comments (1)
Good post Justin.