Technique 2

Feature Flags

Ship the code dark, turn it on later.

When to Use

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:

  • Building something across multiple PRs that would be broken or incomplete if seen mid-flight
  • Validating in production before a full rollout — run the new code for a subset of users first, then expand
  • Coordinating across teams where different groups are building pieces of the same feature and can't coordinate a single-PR launch

The Pattern

  • PR 1: Introduce the feature flag in an "off" state — a single boolean at the decision point
  • PR 2–N: Build the feature behind the flag, one piece at a time. Each PR is shippable because the flag is off — users see nothing different
  • PR N+1: Turn the flag on (flip false to true). This is the launch diff — it's tiny by design
  • PR N+2: Remove the flag and the old code path. Don't skip this step. Flag debt compounds: every live flag doubles your testing surface

Worked Example

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.

1

PR 1: Add useNewSearch feature flag defaulting to false

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.

src/lib/featureFlags.ts
2

PR 2: Implement new weighted search algorithm (unused)

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.

src/lib/search.ts
3

PR 3: Wire search flag to select search implementation

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.

src/components/ProductSearch.tsx
4

PR 4: Enable new search for all users

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.

src/lib/featureFlags.ts
5

PR 5: Remove feature flag and old search implementation

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.

src/components/ProductSearch.tsx

Common Mistakes

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.