Feature Flags: Safe and Controlled Deployments
Feature flags (also called feature toggles or feature switches) are one of the most powerful techniques in modern software engineering. They allow you to decouple deployment from release, giving your team the ability to ship code to production without exposing it to users until you are ready. This simple concept has profound implications for how you build, test, and deliver software.
If you have ever been stuck in a long-lived feature branch, sweating over a risky deployment, or wishing you could instantly roll back a change — feature flags are the answer. In this guide, we will cover the types of feature flags, implementation patterns, gradual rollout strategies, and how to manage the technical debt they can introduce. For related deployment concepts, see Deployment Strategies and CI/CD Pipeline Design.
What Are Feature Flags?
A feature flag is a conditional statement in your code that controls whether a feature is active or inactive. At its simplest, it looks like this:
if (featureFlags.isEnabled("new-checkout-flow")) {
renderNewCheckout();
} else {
renderLegacyCheckout();
}
The flag value can come from a configuration file, a database, an environment variable, or a dedicated feature flag service. The key insight is that the code for both the old and new behavior is deployed together, but only one path is active at runtime.
Types of Feature Flags
Not all feature flags are created equal. Martin Fowler and Pete Hodgson categorize them into four types based on their longevity and dynamism:
| Type | Purpose | Lifespan | Who Controls | Example |
|---|---|---|---|---|
| Release Flags | Decouple deployment from release | Days to weeks | Engineering | Enable new search algorithm |
| Experiment Flags | A/B testing and multivariate tests | Weeks to months | Product/Data | Test two pricing page layouts |
| Ops Flags | Operational control (kill switches) | Indefinite | Operations/SRE | Disable non-critical features under load |
| Permission Flags | Entitlements and premium features | Indefinite | Product/Business | Enable beta features for enterprise tier |
Release Flags
Release flags are the most common type. They allow you to merge incomplete features into your main branch without exposing them to users. This enables trunk-based development, where all developers commit to a single branch and avoid long-lived feature branches.
Release flags should be short-lived. Once the feature is fully rolled out and stable, remove the flag and the old code path. Leaving release flags in place for months is a common source of technical debt.
Experiment Flags
Experiment flags are used for A/B testing. They route different users to different code paths and measure which variant performs better. Unlike release flags, experiment flags are inherently multi-variant — they do not just toggle between on and off, they select among multiple options.
const variant = featureFlags.getVariant("pricing-page-test", userId);
// variant could be "control", "variant-a", or "variant-b"
switch (variant) {
case "variant-a":
return renderNewPricingLayout();
case "variant-b":
return renderMinimalPricingLayout();
default:
return renderCurrentPricingLayout();
}
Ops Flags and Kill Switches
Ops flags give your operations team the ability to degrade gracefully under load. A classic example is a kill switch that disables recommendation engines, search suggestions, or other non-critical features when the system is under stress. These flags are long-lived and should be treated as part of your system's operational controls.
Permission Flags
Permission flags control access to features based on user attributes — subscription tier, role, organization, or geographic location. They are essentially entitlement checks and are typically the longest-lived flags in your system.
Implementation Patterns
There are several ways to implement feature flags, ranging from simple to sophisticated.
Static Configuration
The simplest approach is to use environment variables or configuration files:
# .env file
FEATURE_NEW_CHECKOUT=true
FEATURE_DARK_MODE=false
// Read from environment
const isNewCheckoutEnabled = process.env.FEATURE_NEW_CHECKOUT === "true";
This works for small teams but requires a redeployment to change flag values. It is not suitable for gradual rollouts or user-level targeting.
Database-Backed Flags
Store flag values in a database and read them at runtime. This allows you to change flag values without redeploying, but you need to handle caching and consistency:
class FeatureFlagService {
private cache: Map<string, boolean> = new Map();
private ttl: number = 30000; // 30 seconds
async isEnabled(flagName: string): Promise<boolean> {
if (this.cache.has(flagName)) {
return this.cache.get(flagName)!;
}
const flag = await db.query(
"SELECT enabled FROM feature_flags WHERE name = ?",
[flagName]
);
this.cache.set(flagName, flag.enabled);
setTimeout(() => this.cache.delete(flagName), this.ttl);
return flag.enabled;
}
}
Dedicated Feature Flag Service
For production systems, use a dedicated feature flag service that handles targeting rules, percentage rollouts, and real-time updates. The evaluation flow typically looks like this:
// Using a feature flag SDK
const client = new FeatureFlagClient({
sdkKey: "your-sdk-key",
user: { id: userId, email: userEmail, plan: "enterprise" }
});
// Evaluate flag with user context
const showNewDashboard = client.isEnabled("new-dashboard", {
userId: currentUser.id,
attributes: {
plan: currentUser.plan,
country: currentUser.country,
registeredAt: currentUser.createdAt
}
});
Gradual Rollouts
One of the most valuable capabilities of feature flags is the ability to gradually roll out a feature to increasing percentages of users. A typical rollout plan looks like this:
| Phase | Audience | Percentage | Duration | Goal |
|---|---|---|---|---|
| 1 | Internal team | 0.1% | 1-2 days | Smoke test in production |
| 2 | Beta users | 5% | 3-5 days | Collect feedback |
| 3 | Early adopters | 25% | 1 week | Monitor metrics |
| 4 | General availability | 50% | 1 week | Validate at scale |
| 5 | Full rollout | 100% | Permanent | Complete release |
The key is consistent hashing. You want the same user to always see the same variant. A simple approach is to hash the user ID and flag name together:
function isEnabledForUser(flagName: string, userId: string, percentage: number): boolean {
const hash = murmurhash3(flagName + userId);
const bucket = hash % 100;
return bucket < percentage;
}
// At 25% rollout, users with buckets 0-24 see the feature
// At 50% rollout, users with buckets 0-49 see the feature
// Users who had the feature at 25% still have it at 50%
A/B Testing with Feature Flags
Feature flags are the backbone of A/B testing. The flag service assigns users to variants, and your analytics pipeline measures the impact. A well-structured experiment requires:
- Hypothesis: What you expect to happen ("New checkout will increase conversion by 5%")
- Primary metric: The metric you are optimizing (conversion rate)
- Guardrail metrics: Metrics that must not degrade (page load time, error rate)
- Sample size: Enough users to reach statistical significance
- Duration: Long enough to account for day-of-week effects (typically 1-2 weeks minimum)
For more on data processing pipelines that support A/B testing analytics, see Lambda and Kappa Architecture.
Managing Feature Flag Technical Debt
Feature flags are powerful, but they come with a cost. Every flag is a branch in your code — it increases complexity, makes testing harder, and can lead to subtle bugs when flags interact with each other in unexpected ways.
Flag Lifecycle Management
Establish a clear lifecycle for every flag:
- Creation: Document the flag, its purpose, owner, and expected removal date
- Rollout: Gradually increase the percentage
- Stabilization: Monitor metrics for 1-2 weeks at 100%
- Cleanup: Remove the flag, the old code path, and any targeting rules
Set a policy: release flags must be removed within 30 days of reaching 100% rollout. Track flag age in your dashboards and alert on stale flags.
Testing with Feature Flags
Every flag doubles the number of code paths you need to test. With 10 independent flags, you have 1024 possible combinations. To manage this complexity:
- Test each flag in both states (on and off) independently
- Identify flags that interact and test those combinations explicitly
- Use flag overrides in your test environment to exercise specific paths
- Run integration tests with flags set to their production values
Feature Flag Tools and Platforms
Several mature platforms provide feature flag management out of the box:
| Tool | Type | Key Features | Best For |
|---|---|---|---|
| LaunchDarkly | SaaS | Real-time updates, targeting, experiments | Enterprise teams |
| Unleash | Open Source | Self-hosted, activation strategies | Teams wanting control |
| Flagsmith | Open Source / SaaS | Remote config, segments, audit logs | Flexible deployment |
| Split | SaaS | Feature delivery + experimentation | Data-driven teams |
| ConfigCat | SaaS | Simple setup, percentage rollouts | Small to mid teams |
Best Practices
- Name flags descriptively: Use names like
enable-new-checkout-flowrather thanflag-123 - Centralize flag evaluation: Do not scatter flag checks throughout your codebase — use a single evaluation point per feature
- Log flag evaluations: Record which flags were evaluated and their values for every request — this is essential for debugging
- Set expiration dates: Every release flag should have an owner and a removal deadline
- Avoid flag dependencies: Flag A should not depend on flag B being enabled — this creates invisible coupling
- Use server-side evaluation: Evaluate flags on the server to prevent users from manipulating client-side flags
Feature flags transform how you deliver software. They give you the confidence to deploy frequently, the ability to test in production, and the safety net of instant rollback. But like any powerful tool, they require discipline — create flags deliberately, roll them out carefully, and clean them up promptly. Combined with solid CI/CD pipelines and smart deployment strategies, feature flags are a cornerstone of modern software delivery.