X

How to Implement Feature Flags Without Overengineering

A feature flag lets your application choose between code paths at runtime. You can deploy a new checkout flow without showing it to every customer, or disable an optional integration while you investigate a problem. The benefit is control after deployment; the cost is another condition to test, monitor, and eventually remove.

For an indie product or small team, the goal is not to flag every setting. Start with a clear reason, a safe default, and an owner. If a normal configuration value or a small release solves the problem, use that instead.

Choose the kind of flag you need

The OpenFeature introduction describes flags as runtime decisions used for gradual rollouts, experiments, and graceful degradation. These use cases have different lifetimes and risks:

Use Example What to plan
Release Show a new checkout to a small group first A rollout plan and a date to remove the old path
Operational or kill switch Turn off an optional recommendation service A reliable fallback and an owner who can act during an incident
Experiment Compare two onboarding flows A defined outcome, consistent assignment, and a way to interpret results
Migration Route selected reads to a replacement search or storage system Data consistency, monitoring, and a rollback path

Do not use a flag as an authorization system. Hiding a button in the browser does not prevent a request to a protected endpoint. Enforce access on the server with the application's normal authentication and authorization checks.

Start with the simplest control that works

An environment variable or configuration value is often enough when a setting only differs between development, staging, and production, or when changing it during a deployment is acceptable. For example, NEW_CHECKOUT_ENABLED=false can keep an unfinished path off until the next release.

That is not a gradual rollout to individual users. If you need to change targeting without deploying, divide users into a stable cohort, or keep an auditable history of who changed a flag, a feature-flag service can help. You do not need to operate a service just to add one boolean to a page.

If you want to avoid tying application code to one vendor, OpenFeature defines a provider-neutral evaluation API. A provider connects that API to a flag service or another source. The application code can then evaluate a flag without embedding the provider's rules in every feature.

Here is the shape of a server-side boolean evaluation using the OpenFeature TypeScript API:

import { OpenFeature } from "@openfeature/server-sdk";

type Account = { featureFlagId: string };

const flags = OpenFeature.getClient();

async function showNewCheckout(account: Account): Promise<boolean> {
  return flags.getBooleanValue(
    "checkout.new-flow",
    false,
    { targetingKey: account.featureFlagId },
  );
}

Register and configure a provider before using the client. The false argument is the fallback if no provider is registered; the exact behavior for network or evaluation errors depends on the provider and SDK, so set and test that fallback deliberately. featureFlagId should be a stable identifier intended for this purpose, not an email address or another unnecessary personal detail.

For configuration and secrets, keep the distinction clear: a flag controls behavior, while a secret grants access. Our guide to environment variables and secrets in web projects explains how to keep credentials out of application code and source control.

Roll out in steps

A release flag is most useful when you can change exposure separately from deploying code:

  1. Keep the stable path as the default. Deploy the new implementation behind a disabled flag and check that the existing behavior still works.
  2. Test with a small internal group. Use a test account or controlled environment. Check the user-visible flow and the logs, not just whether the flag evaluates to true.
  3. Expose it gradually. Use the flag service's percentage or segment targeting so the same user does not switch between versions unpredictably. Start with a small cohort, inspect errors and support reports, then expand when the evidence is acceptable.
  4. Keep a rollback action ready. Confirm who can turn the flag off, how quickly the application receives the change, and what users see when the new path is disabled.
  5. Remove the temporary flag. Once the release is complete and the old path is no longer needed, delete the condition and its obsolete code.

For example, Unleash activation strategies describe percentage rollouts based on a user's unique identifier, which keeps the assignment stable. The details vary by provider; confirm its evaluation, caching, and update behavior before relying on a flag during an incident.

Pick safe defaults and failure behavior

Decide what the application should do when a flag is missing, has an invalid value, or cannot be evaluated. For a new release, the safer fallback is usually the existing, known behavior. For a kill switch, decide which optional work should stop and what response remains available to the user.

Do not assume that a remote flag service is always reachable or that a flag change instantly reaches every running process. Check the SDK's documented fallback and caching behavior. For example, LaunchDarkly's guide to testing flagged code treats the fallback value as a scenario to test when evaluation fails.

A kill switch is only useful if the affected code can safely stop. If a background job has already charged a customer or written data, flipping a flag cannot undo that action. Keep irreversible work idempotent where possible, and use the application's normal recovery process for data or payments.

Keep targeting private and predictable

Target only on attributes the feature actually needs. A stable internal identifier is often enough for percentage rollout; use a plan or region only when the feature genuinely depends on it. Do not send email addresses, sensitive traits, or full user records merely because a flag tool accepts them.

OpenFeature's evaluation-context guidance notes that providers may serialize or persist context, and recommends care with personally identifiable information. Hashing an email does not automatically make it anonymous. Check the provider's data handling and retention terms, and filter context before it leaves your application if the SDK supports that.

For an experiment, a flag only selects a variation; it does not prove that the variation improved the product. Decide what you will measure before launch, keep assignment consistent, and avoid changing several parts of the experience at once. For a broader approach to measuring conversion changes, see our guide to A/B testing and sales-funnel optimization.

Test both sides and the fallback

At a minimum, test:

  • The flag enabled and disabled.
  • A missing or unavailable provider and the configured fallback.
  • Targeting at the boundary, such as a user who should and should not enter a rollout.
  • The main error and recovery paths in the new behavior.
  • The production state you have now and the state you plan to release next.

You usually do not need a test for every possible combination of every flag. That quickly becomes unmanageable. Select the states that affect current behavior, the next release, fallback handling, and any combination that has caused trouble before. Use a test provider or a mock so results are repeatable; do not make a unit test depend on a live flag service.

Give each flag an owner and an end date

Keep a small registry or use the metadata in your flag service. Record the flag name, purpose, owner, default behavior, date created, and review or removal date. A short name such as checkout.new-flow is more useful than flag-17, and a clear owner makes cleanup actionable.

Temporary release and experiment flags should have an expected end date. Operational switches may need to stay, but they still need an owner and a review. For one concrete example, Unleash's flag documentation assigns expected lifetimes to flag types and marks flags potentially stale when those lifetimes pass. Those are Unleash defaults, not a universal schedule; choose a review period that fits your release cycle.

When a flag is ready to retire, decide which behavior becomes permanent, remove the unused branch, and delete the flag from the service. Keeping old code “just in case” leaves extra states that future changes and tests must account for.

When a flag is more work than a release

Skip a feature flag when:

  • The change is small and can be deployed normally.
  • You do not need to hide or gradually expose the feature.
  • A single environment setting is enough.
  • Nobody will monitor the rollout or own the cleanup.
  • The flag adds more possible combinations than the feature is worth.

Flags are useful when they create a specific operational or product control. They are not a substitute for clear releases, authorization checks, observability, or a rollback plan. Keep the number small, test the default and failure cases, and remove temporary flags when their job is done.

Sources

Categories: Development
Related Post