Introduction
Feature flags start simple. You add an if (featureEnabled) check, wrap a new code path, and ship with confidence. You can turn it off if something goes wrong. Easy.
Then, six months later, your codebase has 200 flags. Nobody knows which ones are still active. A junior engineer deletes a flag that turned out to be load-bearing. A customer-facing bug exists because two flags interact in a way nobody anticipated. Your QA team has given up trying to test every combination.
Feature flags are one of the most powerful tools in modern software delivery — and one of the most commonly mismanaged. This post is about doing them right, from day one.
What Feature Flags Actually Are
At their core, a feature flag (also called a feature toggle, feature switch, or feature gate) is a mechanism that allows you to change application behavior without deploying new code.
But that one-liner undersells what they enable:
- Continuous delivery without continuous release — merge code to main before it's ready for users
- Targeted rollouts — release to 1% of users, then 10%, then everyone
- Kill switches — instantly disable a feature if it causes problems in production
- A/B testing — run experiments on real user traffic
- Ops flags — tune system behavior (timeouts, cache sizes, rate limits) without a deploy
- Entitlements — gate features behind subscription tiers or permissions
The same underlying mechanism serves very different purposes. Treating all flags the same is the first mistake teams make.
The Four Types of Feature Flags
Martin Fowler's taxonomy is the best mental model here. There are four distinct kinds of flags, and they have different lifespans, owners, and risk profiles.
1. Release Flags (short-lived)
Used to hide incomplete features from users while development continues. These flags exist to decouple deploy from release. They should live for days or weeks — not months.
Example: You're building a new checkout flow. You merge it behind a flag, keep iterating, and enable it when it's done.
2. Experiment Flags (short-lived)
Used for A/B tests and multivariate experiments. They exist to answer a question — once the question is answered, the flag should be removed and the winning variant made permanent.
Example: Test whether a new CTA button color improves conversion. After statistical significance is reached, pick a winner and delete the flag.
3. Ops Flags (long-lived)
Used by operations teams to control system behavior. These can be long-lived by design, but they should be owned by ops, not forgotten by a developer who left the company.
Example: A flag that controls the size of a connection pool, or enables a fallback data source when the primary is degraded.
4. Permission Flags (long-lived)
Used to enable features for specific users, roles, or subscription tiers. These are essentially part of your authorization system.
Example: A "Pro" feature only visible to paying customers, or an admin panel only available to internal users.
Common Mistakes (and How to Avoid Them)
Mistake 1: Flags that never die
The most common problem. A release flag gets shipped, the feature goes live, and nobody removes the flag. Six months later there are 150 flags and nobody knows what's safe to delete.
Fix: Every flag gets a creation date and an owner. Release and experiment flags get an expiry date — a ticket created at flag-creation time to remove it. Make flag cleanup part of your definition of done.
Mistake 2: Flags with no owner
If a flag belongs to everyone, it belongs to no one. When something breaks, nobody knows who to ask.
Fix: Every flag has a named owner — a team or individual responsible for its lifecycle. Store this in your flag management system.
Mistake 3: Testing combinations is impossible
With N boolean flags, you have 2^N possible states. Even with 10 flags, that's 1,024 combinations. Most of them will never be tested.
Fix: Be disciplined about flag scope. Flags should be independent where possible. Avoid business logic that depends on multiple flags being in specific states simultaneously. If you find yourself writing if (flagA && !flagB && flagC), something has gone wrong.
Mistake 4: Flags in the database, evaluated everywhere
Checking flag state by querying a database on every request is a performance disaster. But evaluating flag logic inline across hundreds of files is a maintainability disaster.
Fix: Use a dedicated feature flag service (LaunchDarkly, Unleash, Flagsmith, or build a simple one) with in-memory caching. Centralize flag evaluation logic. Your application code should call a single isEnabled("flag-name", context) function — never raw database queries.
Mistake 5: Flags as config for everything
Feature flags are not a general-purpose configuration system. Using them to store API keys, service URLs, or application settings creates confusion about what a "flag" is.
Fix: Keep flags for behavioral toggles. Use environment variables or a proper config system for infrastructure configuration.
Implementing Flags Properly
The flag evaluation contract
Your flag evaluation should always accept a context — the user, request, or environment being evaluated. A flag's value isn't global; it can vary based on who's asking.
// Bad — global boolean
if (flags.NEW_CHECKOUT) { ... }
// Good — context-aware evaluation
if (flagService.isEnabled('new-checkout', { userId, accountTier, region })) { ... }
Targeting rules
Good flag systems support targeting rules beyond simple on/off:
- User targeting: Enable for specific user IDs (useful for internal testing)
- Percentage rollouts: Enable for X% of users, consistently (same user always gets same experience)
- Attribute targeting: Enable for users in a specific region, on a specific plan, or using a specific version of your app
- Environment targeting: On in staging, off in production — or vice versa
Gradual rollouts
A gradual rollout is one of the most valuable patterns. Instead of flipping a flag from 0% to 100%, you go 1% → 5% → 20% → 50% → 100%, monitoring error rates and key metrics at each step.
This gives you a production safety net that no amount of staging testing can replicate.
Flag state should be observable
You should be able to answer, at any moment: "What flags is this specific user seeing?" This is essential for debugging production issues. Log flag evaluations, and build tooling to look up a user's flag state.
The Flag Lifecycle
A healthy flag lifecycle looks like this:
1. Create flag → define type, owner, expiry (for release/experiment flags)
2. Deploy behind flag (dark launch)
3. Enable for internal users / QA
4. Gradual rollout (1% → 10% → 50% → 100%)
5. Monitor metrics and error rates at each step
6. Full rollout
7. Remove flag from code → delete flag from system
Step 7 is the one most teams skip. Removing the flag is part of shipping the feature. Until the flag is gone, you have dead code paths, unnecessary complexity, and a ticking time bomb.
Tooling
You don't always need to buy a SaaS tool. Here's a rough guide:
| Team Size | Recommendation |
|---|---|
| Solo / tiny team | Environment variables or a simple config file |
| Small team (<20 engineers) | Self-hosted Unleash or Flagsmith (open source) |
| Mid-size team | LaunchDarkly, Statsig, or GrowthBook for experiments |
| Large / enterprise | LaunchDarkly, Optimizely, or build internal tooling |
The key features to look for: targeting rules, gradual rollouts, audit logs, SDKs for your stack, and a UI for non-engineers to manage flags.
Summary
Feature flags are powerful, but they accrue debt faster than almost any other pattern. The teams that use them well treat flag management as a first-class engineering concern — not an afterthought.
The rules:
- Every flag has a type, an owner, and an expiry date (for short-lived flags)
- Centralize flag evaluation — never scatter flag logic across the codebase
- Use context-aware evaluation for targeted rollouts
- Monitor during rollouts — gradual rollouts only help if you're watching
- Delete flags aggressively — if it's 100% on and has been for a month, remove it
- Flags are not config — don't store infrastructure settings in your feature flag system
Done right, feature flags give you superpowers. Done wrong, they're a landmine farm. The difference is discipline.
Top comments (0)