Ship the code dark, turn it on later.
The feature is user-facing and you want to deploy the implementation incrementally without exposing incomplete work to users. You'll recognize the need for this when the feature requires multiple PRs to build but users would see a broken or half-built experience if they encountered partial work. The flag lets you merge to main continuously while keeping the feature invisible until it's ready.
Use this technique when:
false to true). This is the launch diff — it's tiny by designHow this looks in your git history:
The team is replacing the search algorithm on an e-commerce product listing page. The current search is a simple string-match filter; the new search will use a weighted scoring algorithm that ranks results by relevance across multiple fields.
We need to ship this incrementally — the new algorithm is complex enough that it needs validation before going live. A feature flag lets us merge the implementation without exposing it to users, then flip it on when we're confident.
Create a feature flags module with a typed FeatureFlag union and a record of current flags. Adding the flag in the "off" state is the entire PR.
Nothing in the app calls isEnabled yet — this PR is purely additive. It establishes the contract for toggling behavior without changing any existing behavior.
| line number | line content |
|---|
Add the new weightedSearch() function alongside the existing logic, now extracted as simpleSearch(). At this point, nothing calls weightedSearch() — it's dark code that ships to production but affects no user.
The new algorithm scores each product across multiple fields: name match (highest weight), category, description, and tags. A prefix bonus rewards exact-match starts.
This PR ships to production but zero users see the new behavior. The flag is still off.
| line number | line content |
|---|
Modify the search component to check the feature flag and route to the appropriate search function. The old inline filter is removed; the flag now selects between the two implementations at runtime.
The behavior for all users is unchanged — the flag is still false, so simpleSearch() is called. But the wiring is now in place.
| line number | line content |
|---|
Change useNewSearch from false to true. This is the entire PR.
This is the payoff of the technique: the launch is a one-line diff. No feature code changes, no risk of accidental regressions — just a config flip. If something goes wrong in production, the rollback is equally trivial.
| line number | line content |
|---|
Delete featureFlags.ts entirely, remove simpleSearch() from search.ts, and simplify the component to call weightedSearch() directly. The diff shown here is for ProductSearch.tsx; the full cleanup also removes featureFlags.ts and the dead simpleSearch() function.
This cleanup PR is not optional — flags left in place become permanent fixtures and their "off" branch slowly rots. Remove flags within 2 weeks of full rollout.
| line number | line content |
|---|
Forgetting to clean up flags after full rollout. Flag debt is real — every live flag doubles your code paths and testing surface. Set a calendar reminder to remove the flag within 2 weeks of full rollout. PR 5 in this example is not optional; it's part of the technique.
Making the flag too coarse-grained. One flag controlling too many behaviors creates large, hard-to-test toggles. Each independently shippable behavior should have its own flag.
Not testing both flag states in CI. If you only test with the flag off, you won't catch regressions in the new code path until you flip it in production. Run your test suite with the flag in both states.