Feature toggles (also known as feature flags) are a powerful technique that allows teams to modify system behavior without changing code. They enable separating code deployment from feature releases — a critical capability for Continuous Delivery.
Why Feature Toggles
Imagine this scenario: a team is working on a new algorithm in a critical part of the system. The changes will take several weeks. The traditional approach is to create a feature branch and merge it after completion. But long-lived branches lead to painful merge conflicts.
The alternative is to work in trunk (main branch), hiding incomplete functionality behind a feature toggle:
function reticulateSplines() {
if (featureIsEnabled("use-new-algorithm")) {
return enhancedSplineReticulation();
} else {
return oldFashionedSplineReticulation();
}
}
Now the code can be deployed to production every day, while the new algorithm remains hidden from users until it's fully ready. This is the key principle of Continuous Delivery: separating code deployment from feature release.
Four Categories of Toggles
Not all toggles are created equal. They differ along two key dimensions: longevity (how long the toggle will exist) and dynamism (how often the toggle decision changes).
Release Toggles
Allow deploying incomplete code to production as hidden (latent) functionality.
- Longevity: short (days to weeks)
- Dynamism: static (same decision for all users)
- Managed by: developers or product managers
Example: A new "Delivery Date Estimation" feature is implemented for only one shipping partner. The product manager wants to wait until all partners are integrated before release. The feature is hidden behind a release toggle.
Experiment Toggles
Used for A/B testing. Each user is assigned to a cohort and sees the corresponding variant of functionality.
- Longevity: medium (weeks — until statistically significant results)
- Dynamism: high (decision made per-request based on user ID)
- Managed by: product team, analysts
Example: The team debates whether a new recommendation algorithm will increase conversion. Instead of arguing, they launch an A/B test: 50% of users see the old algorithm, 50% see the new one. After two weeks, the data shows which variant performs better.
// Cohort assignment based on user ID
function getUserCohort(userId, experimentName) {
const hash = hashCode(userId + experimentName);
return hash % 100 < 50 ? 'control' : 'treatment';
}
Ops Toggles
Give operators the ability to quickly change system behavior in production. Often called "kill switches."
- Longevity: short to permanent (for kill switches)
- Dynamism: static, but requires instant switching
- Managed by: ops team, SRE
Example: Under high load, you can disable the heavy recommendations panel on the homepage. This preserves availability of core functionality.
Permission Toggles
Control access to features for specific user groups: premium subscribers, beta testers, internal users.
- Longevity: very long (years)
- Dynamism: high (per-request decision)
- Managed by: product, business
Example: "Champagne Brunch" — a practice where new features are first available only to company employees (dogfooding), then to beta users, and only then to everyone.
function canAccessFeature(user, featureName) {
if (user.isEmployee) return true;
if (user.isBetaTester && betaFeatures.includes(featureName)) return true;
if (user.isPremium && premiumFeatures.includes(featureName)) return true;
return generallyAvailableFeatures.includes(featureName);
}
Implementation Techniques
Feature toggles can turn code into spaghetti if you scatter if/else checks throughout the codebase. Here are techniques to avoid this.
Separate Decision Point from Logic
Bad — toggle logic scattered through code:
// Bad: magic string and direct config access
function generateInvoiceEmail() {
const baseEmail = buildEmailForInvoice(this.invoice);
if (features.isEnabled("next-gen-ecomm")) {
return addOrderCancellationContent(baseEmail);
}
return baseEmail;
}
Better — encapsulated decisions:
// Good: decisions in one place
const featureDecisions = {
includeOrderCancellationInEmail() {
return features.isEnabled("next-gen-ecomm");
}
};
function generateInvoiceEmail() {
const baseEmail = buildEmailForInvoice(this.invoice);
if (featureDecisions.includeOrderCancellationInEmail()) {
return addOrderCancellationContent(baseEmail);
}
return baseEmail;
}
Inversion of Control (IoC)
Even better — pass the decision through configuration:
function createInvoiceEmailer(config) {
return {
generateInvoiceEmail() {
const baseEmail = buildEmailForInvoice(this.invoice);
if (config.includeOrderCancellation) {
return addOrderCancellationContent(baseEmail);
}
return baseEmail;
}
};
}
// Feature-aware factory
function createFeatureAwareFactory(featureDecisions) {
return {
invoiceEmailer() {
return createInvoiceEmailer({
includeOrderCancellation: featureDecisions.includeOrderCancellationInEmail()
});
}
};
}
Now InvoiceEmailer doesn't know toggles exist. It's easy to test in isolation.
Strategy Pattern
For long-lived toggles or multiple toggle points, use the Strategy pattern:
function createInvoiceEmailer(contentEnhancer) {
return {
generateInvoiceEmail() {
const baseEmail = buildEmailForInvoice(this.invoice);
return contentEnhancer(baseEmail);
}
};
}
// Strategies
const addOrderCancellation = (email) => addOrderCancellationContent(email);
const noOp = (email) => email;
// Strategy selection at creation time
const emailer = featureDecisions.includeOrderCancellationInEmail()
? createInvoiceEmailer(addOrderCancellation)
: createInvoiceEmailer(noOp);
Toggle Configuration
There's a spectrum of approaches for storing configuration — from simple to complex:
| Approach | Description | When to Use |
|---|---|---|
| Hardcoded | Commenting/uncommenting code | Local development only |
| Environment variables | Env vars | Release toggles, simple cases |
| Config file | JSON/YAML file in repository | Release toggles with versioning |
| Database | DB table + admin UI | Dynamic toggles, A/B tests |
| Distributed config | Consul, etcd, Zookeeper | Ops toggles, microservices |
| Feature flag service | LaunchDarkly, Unleash, Flipper | All categories, teams without DevOps |
Lifecycle Management
Toggles tend to accumulate. Each toggle adds conditional logic, increases testing scope, and cognitive load. Without discipline, the system becomes a maze.
Management Practices
- Removal task: When creating a release toggle, immediately create a backlog task for its removal.
- Expiration date: Add to toggle metadata the date by which it should be removed.
-
Time bomb: Tests fail if the toggle isn't removed by a certain date:
test('toggle should be removed', () => { const expirationDate = new Date('2024-03-01'); if (new Date() > expirationDate) { throw new Error('Toggle "new-checkout" should have been removed!'); } }); - Toggle limit: Set a maximum number of active toggles. To add a new one — first remove an old one.
Testing Systems with Toggles
Minimum set of configurations to test:
- Configuration expected in production (all new features enabled)
- Fallback configuration (all new features disabled)
- Configuration with all toggles enabled (if applicable)
Don't test every combination — that's combinatorial explosion. Most toggles don't interact with each other.
Summary
Feature toggles are not just if/else. They're a tool that requires understanding and discipline.
| Category | Lifespan | Dynamism | Managed By |
|---|---|---|---|
| Release | Days to weeks | Static | Developers |
| Experiment | Weeks | Per-request | Product, analysts |
| Ops | Days to years | Static, fast switching | Ops, SRE |
| Permission | Years | Per-request | Product, business |
Key principles:
- Different toggle categories require different management
- Encapsulate decision-making logic
- Use IoC to isolate code from toggles
- Actively remove toggles — they shouldn't accumulate
- Start with simple configuration solutions