CICD Feature Flags
Introduction
Feature flags (also known as feature toggles, feature switches, or feature flippers) are a powerful technique in modern software development that allows teams to modify system behavior without changing code. When integrated into a CI/CD (Continuous Integration/Continuous Deployment) pipeline, feature flags enable developers to deploy code to production that may not be ready for all users, effectively separating code deployment from feature release.
Think of feature flags as light switches in your code that you can turn on or off remotely. This gives you incredible flexibility to:
- Deploy features incrementally to specific user groups
- Test features in production with minimal risk
- Roll back problematic features without redeploying code
- Conduct A/B testing to compare different implementations
This article will guide you through understanding and implementing feature flags in your CI/CD pipeline, even if you're just getting started with development practices.
Understanding Feature Flags
What is a Feature Flag?
At its core, a feature flag is a conditional statement in your code that determines whether a particular feature is enabled or disabled. Here's a simple example:
// Simple feature flag implementation
if (featureFlags.isEnabled('new-search-algorithm')) {
// Use the new search algorithm
return newSearchAlgorithm(query);
} else {
// Use the existing search algorithm
return currentSearchAlgorithm(query);
}
The conditional check featureFlags.isEnabled('new-search-algorithm')
acts as a switch that can be turned on or off without changing the code itself. The state of this switch is typically stored in a configuration file, database, or managed through a feature flag service.
Types of Feature Flags
Feature flags can be categorized based on their lifecycle and purpose:
- Release Flags: Used to hide incomplete or untested features from users
- Experiment Flags: Used for A/B testing to measure impact
- Ops Flags: Used to control operational aspects like system performance
- Permission Flags: Used to provide features to specific user segments
Feature Flags in the CI/CD Pipeline
How Feature Flags Enhance CI/CD
A typical CI/CD pipeline without feature flags faces a challenge: code must be completely ready before it can be deployed to production. This can lead to:
- Long-lived feature branches
- Delayed integration
- Large, risky deployments
With feature flags, the CI/CD pipeline becomes more flexible:
This allows teams to:
- Merge code more frequently, even if features aren't complete
- Deploy code to production in a disabled state
- Enable features gradually or for specific users
- Roll back problematic features without code changes
Integration Points in the Pipeline
Feature flags interact with your CI/CD pipeline at several key points:
- Development: Developers create flags to isolate new functionality
- Testing: QA can test both with flags on and off
- Deployment: Code deploys with flags off for new features
- Release: Features are enabled via flag configuration, not code deployment
- Monitoring: Usage and errors are tracked by flag state
Implementing Feature Flags
Basic Implementation
Let's implement a simple feature flag system in a Node.js application:
// feature-flags.js - A simple feature flag service
class FeatureFlags {
constructor() {
this.flags = {
'new-login-page': false,
'advanced-search': false,
'dark-mode': true
};
}
isEnabled(featureName) {
return this.flags[featureName] === true;
}
enable(featureName) {
this.flags[featureName] = true;
}
disable(featureName) {
this.flags[featureName] = false;
}
}
module.exports = new FeatureFlags();
Using this service in your application:
// app.js
const featureFlags = require('./feature-flags');
const express = require('express');
const app = express();
app.get('/search', (req, res) => {
if (featureFlags.isEnabled('advanced-search')) {
// New advanced search functionality
return res.json(performAdvancedSearch(req.query.term));
} else {
// Current search functionality
return res.json(performBasicSearch(req.query.term));
}
});
// Other routes and server setup...
More Advanced Implementation
For production applications, you'll want a more robust solution that can:
- Load flags from external configuration
- Update flag states without redeployment
- Target specific users or user segments
Here's a more sophisticated example:
// advanced-feature-flags.js
class AdvancedFeatureFlags {
constructor() {
this.flags = {};
this.loadFlags();
// Refresh flags every 5 minutes
setInterval(() => this.loadFlags(), 5 * 60 * 1000);
}
async loadFlags() {
try {
// Load from database, API, or configuration service
const response = await fetch('https://config-server/feature-flags');
this.flags = await response.json();
console.log('Feature flags refreshed');
} catch (error) {
console.error('Failed to refresh feature flags:', error);
}
}
isEnabled(featureName, userId = null) {
const flag = this.flags[featureName];
if (!flag) return false;
// Simple boolean flag
if (typeof flag === 'boolean') {
return flag;
}
// Percentage rollout
if (flag.percentage) {
// Generate a consistent hash based on feature name and user ID
const hash = this.hashUser(featureName, userId);
return (hash % 100) < flag.percentage;
}
// User targeting
if (flag.enabledForUsers && userId) {
return flag.enabledForUsers.includes(userId);
}
return false;
}
hashUser(featureName, userId) {
// Simple hash function for demo purposes
if (!userId) return Math.random() * 100;
let hash = 0;
const combinedString = featureName + userId;
for (let i = 0; i < combinedString.length; i++) {
hash = ((hash << 5) - hash) + combinedString.charCodeAt(i);
hash |= 0; // Convert to 32bit integer
}
return Math.abs(hash) % 100;
}
}
module.exports = new AdvancedFeatureFlags();
Popular Feature Flag Services
While you can build your own feature flag system, there are many production-ready solutions available:
- LaunchDarkly: Comprehensive feature flag service with advanced targeting
- Split.io: Feature experimentation platform with analytics
- Flagsmith: Open-source feature flag and remote config service
- CloudBees Feature Management: Enterprise-grade feature management
- Firebase Remote Config: For mobile and web applications
Most of these services provide SDKs for various programming languages and frameworks.
Best Practices for Feature Flags in CI/CD
Organizing Feature Flags
- Naming Conventions: Use clear, consistent names like
feature-name-action
- Documentation: Document the purpose and expected lifetime of each flag
- Categorization: Group flags by purpose (release, experiment, ops)
Flag Lifecycle Management
Feature flags should not live forever. Implement a process to:
- Review flags regularly: Schedule reviews of active flags
- Remove obsolete flags: Once a feature is fully adopted, remove its flag
- Track flag usage: Monitor which code paths use which flags
For example, you might add creation dates and owners to your flags:
const flags = {
'new-checkout-process': {
enabled: false,
createdDate: '2023-04-15',
owner: 'payment-team',
expectedRemovalDate: '2023-07-01'
}
};
Testing with Feature Flags
Testing becomes more complex with feature flags. You should:
- Test both states: Ensure your application works with the flag on or off
- Test transitions: Verify that toggling a flag at runtime works correctly
- Include flag state in test reports: Log which flag states were tested
Example Jest test for feature flag functionality:
// checkout.test.js
const featureFlags = require('./feature-flags');
const checkoutService = require('./checkout-service');
describe('Checkout Process', () => {
test('should use new process when flag is enabled', () => {
// Setup
featureFlags.enable('new-checkout-process');
// Test
const result = checkoutService.processOrder({items: [{id: 1, qty: 2}]});
// Assert
expect(result.processingMethod).toBe('new');
});
test('should use old process when flag is disabled', () => {
// Setup
featureFlags.disable('new-checkout-process');
// Test
const result = checkoutService.processOrder({items: [{id: 1, qty: 2}]});
// Assert
expect(result.processingMethod).toBe('legacy');
});
});
Real-World Examples
Gradual Rollout for a New UI
Imagine you're updating your application's dashboard with a completely new design. Instead of releasing it to all users at once, you can use feature flags for a gradual rollout:
// Dashboard component
import React from 'react';
import { useFeatureFlags } from './feature-flags-hook';
function Dashboard() {
const { isEnabled } = useFeatureFlags();
if (isEnabled('new-dashboard-ui', getUserId())) {
return <NewDashboard />;
} else {
return <CurrentDashboard />;
}
}
With the corresponding flag configuration:
{
'new-dashboard-ui': {
// Week 1: Internal users only
enabledForUsers: ['internal-team-1', 'internal-team-2'],
// Week 2: 5% of users
percentage: 5,
// Week 3: 20% of users
// percentage: 20,
// Week 4: 50% of users
// percentage: 50,
// Week 5: All users
// percentage: 100,
}
}
This approach allows you to:
- Test with internal users first
- Monitor performance and errors at each stage
- Roll back if issues arise
- Gradually increase exposure until all users have the new UI
Feature Flag for a New Payment Provider
When integrating a new payment provider, you might want to test it with real transactions but limit exposure:
function processPayment(order) {
if (featureFlags.isEnabled('new-payment-provider', order.userId)) {
try {
return newPaymentProvider.process(order);
} catch (error) {
// Log the error
logger.error('New payment provider failed', error);
// Fall back to the existing provider
return currentPaymentProvider.process(order);
}
} else {
return currentPaymentProvider.process(order);
}
}
This implementation:
- Only routes some users to the new payment provider
- Includes a fallback mechanism if the new provider fails
- Logs errors for monitoring
Common Challenges and Solutions
Challenge: Flag Explosion
Problem: Too many flags make code hard to follow and maintain.
Solution:
- Regularly audit and remove unused flags
- Use a hierarchical flag structure
- Implement automated tracking of flag usage in code
Challenge: Configuration Drift
Problem: Different environments have different flag configurations, causing inconsistent behavior.
Solution:
- Keep a version-controlled default configuration
- Automate configuration deployment
- Log and alert on configuration changes
Challenge: Testing Complexity
Problem: Feature flags multiply the test cases needed.
Solution:
- Automate testing of both flag states
- Use feature flag testing frameworks
- Prioritize test coverage for critical paths
Summary
Feature flags are a powerful tool for modern CI/CD pipelines, allowing teams to:
- Decouple deployment from release: Deploy code without releasing features
- Reduce risk: Test features in production with limited exposure
- Increase deployment frequency: Merge incomplete features without breaking production
- Enable experimentation: Easily conduct A/B tests and gather user feedback
When implemented correctly and managed properly, feature flags can significantly improve your team's ability to deliver software continuously and confidently.
Additional Resources
Libraries and Tools
- JavaScript: unleash-client, flagsmith
- Python: pytest-feature-flag
- Java: ff4j
- Go: flipt
Learning Resources
- Martin Fowler's article on Feature Toggles
- "Release It!" by Michael T. Nygard (Chapter on Decoupling Deployment and Release)
- "Accelerate" by Nicole Forsgren et al. (Research on CI/CD practices)
Exercises
- Basic Implementation: Create a simple feature flag system in your preferred language.
- Integration Exercise: Add feature flags to an existing project to hide a new feature.
- Targeting Exercise: Implement user targeting based on user properties.
- Management Exercise: Design a process for reviewing and removing old feature flags.
- Testing Challenge: Write tests that verify your application works correctly with different flag configurations.
By mastering feature flags in your CI/CD pipeline, you'll gain more control over your software releases and be able to deliver features to users more safely and confidently.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)