A feature flag (or feature toggle) is a runtime switch that controls whether a code path is active for a given user, tenant, or environment. The code ships in the build, but exposure is decided later without a new deploy.
Feature flags separate deployment from release. Deployment puts code in production. Release decides who sees the behavior.
Why teams use them
| Goal | What the flag enables |
|---|---|
| Safer rollout | Turn a feature on for internal users, then a percentage, then everyone |
| Fast rollback | Disable bad behavior in seconds without redeploying |
| Experimentation | Compare variants without maintaining long-lived branches |
| Long-lived integration | Merge incomplete work behind a default-off flag |
| Operational control | Kill switches for risky integrations or expensive paths |
CI/CD pipelines still build and promote artifacts. Feature flags change how much of that artifact is visible after it reaches production.
flowchart LR Build["Build artifact"] --> Deploy["Deploy to production"] Deploy --> Code["Code with flag checks"] FlagService["Flag service"] --> Code Code -->|"Flag off"| OldPath["Existing behavior"] Code -->|"Flag on"| NewPath["New behavior"] Metrics["Metrics and logs"] --> Observe["Observe rollout"]
Common flag types
- Release flag, gates a new feature until the team is confident. Remove after full rollout.
- Ops flag, kill switch for load, dependency failure, or incident response. May stay long-lived.
- Experiment flag, routes users to variants for A/B tests. Needs measurement and an end date.
- Permission flag, exposes capability to specific tenants, plans, or roles.
Treat release and experiment flags as temporary. Permanent if (flag) branches become hidden configuration debt.
Rollout patterns
Feature flags complement deployment strategies from CI/CD:
| Approach | What moves | Best fit |
|---|---|---|
| Canary deploy | Traffic to a new binary | Infra or runtime version changes |
| Feature flag | Exposure inside one binary | Product behavior behind existing deploy |
| Blue-green | Whole environment swap | Fast binary rollback |
| Flag percentage rollout | User subset sees new logic | Gradual feature release with observability |
A canary tests a new build. A flag tests new behavior inside a build that is already live. Teams often combine both on risky launches.
Implementation basics
Flags need a consistent evaluation path:
- Central evaluation, app asks a flag service or config store instead of scattering env vars.
- Stable targeting, user ID, tenant ID, or session key should get the same result across requests.
- Defaults, fail safe when the flag provider is unavailable. Usually default to off for new features.
- Auditability, record who changed a flag, when, and what percentage applied.
async function createInvoice(user, cart) {
const useNewPricing = await flags.isEnabled("new-pricing-engine", {
userId: user.id,
tenantId: user.tenantId,
});
if (useNewPricing) {
return newPricingService.createInvoice(cart);
}
return legacyPricingService.createInvoice(cart);
}async function createInvoice(user: User, cart: Cart) {
const useNewPricing = await flags.isEnabled("new-pricing-engine", {
userId: user.id,
tenantId: user.tenantId,
});
if (useNewPricing) {
return newPricingService.createInvoice(cart);
}
return legacyPricingService.createInvoice(cart);
}Keep flag checks at clear boundaries such as service entry points or route handlers. Deep nesting across many files makes removal harder.
Observability and rollout discipline
Flag rollouts need the same evidence as deploys. Use observability to compare error rate, latency, saturation, and business metrics between enabled and disabled cohorts.
Practical habits:
- Define success metrics before enabling the flag for real users.
- Roll out in stages, internal, small percentage, wider audience.
- Watch support volume and downstream side effects, not only HTTP 500s.
- Keep a documented rollback step, usually set flag to off.
- Remove the flag and dead code after the feature is fully released.
A flag without monitoring is just a hidden deploy with extra steps.
Lifecycle and cleanup
Every flag should have an owner and a expected lifetime.
- Create with default off and a short description of risk.
- Test in staging and with internal accounts.
- Roll out gradually with metrics attached.
- Make the feature generally available.
- Delete the flag and simplify the code path.
Old flags confuse readers and increase test surface. Schedule cleanup like any other production change.
Trade-offs
- Flags add branching, testing combinations, and dependency on a flag service.
- Long-lived flags make code harder to reason about and can hide unfinished design.
- Misconfigured targeting can expose features to the wrong tenants.
- Experiment flags need statistical discipline, not only engineering toggles.
- They reduce redeploy rollback pressure but do not replace good CI/CD or infrastructure as code practices.
Feature flags are most valuable when exposure is intentional, measured, and temporary. Deploy continuously, release deliberately, and remove flags once the decision is made.
