Feature Flags Are Not Config. Treating Them That Way Breaks You.
For: A mid-level engineering lead at a 40-person B2B SaaS company who inherited a codebase where every feature flag, kill switch, ops config value, and A/B test variant is stored in the same environment variable file or database table — and is now untangling a production incident caused by someone toggling the wrong key
A feature flag is not a config value. It is a time-bound decision with an owner, a target cohort, and an expiry date. The moment you store it next to your Redis host and your Stripe webhook secret, you have thrown away the metadata that makes flags safe to change in production. That is why your "simple" toggle keeps causing incidents: you built one primitive to serve three different jobs, and none of them are being done well.
If you inherited a codebase where ENABLE_NEW_CHECKOUT, DB_POOL_SIZE, and EXPERIMENT_PRICING_V2_VARIANT all live in the same env file or app_settings table, this post is for you.
The problem: three primitives, one storage bucket
Walk through any incident postmortem involving a bad toggle and you will find the same root cause: someone treated a feature flag like config, or config like a flag, because the system did not distinguish between them. Here is what actually lives in that shared bucket:
- Feature flags — release toggles that gate unfinished code. Meant to be short-lived (weeks). Owned by the engineer who wrote the feature. Should die when the feature ships to 100%.
- Operational config — database URLs, pool sizes, timeouts, third-party API keys. Long-lived. Owned by platform/infra. Changing them is a deploy-adjacent activity.
- Kill switches — circuit breakers around risky subsystems (payments, a flaky vendor, an expensive query). Long-lived. Owned by on-call. Flipped under duress, often at 3am.
- Experiment variants — A/B assignments for product decisions. Bound to a cohort, a metric, and a statistical window. Owned by product/analytics.
- Entitlements — what a customer's plan can do. Owned by billing/commercial. Not a flag at all, but often stuffed in the same table.
Each has a different lifecycle, different blast radius, different reviewers, and different rollback semantics. Storing them together is like keeping your passport, your grocery list, and a live grenade in the same drawer. The drawer works fine until someone reaches in fast.
The insight: flags are decisions, not state
Config is state. It describes how the system is currently wired. You change it when the wiring changes.
A feature flag is a decision: "we have decided that users in cohort X should see behavior Y until date Z, at which point this flag and all the code paths behind it should be deleted." That decision has metadata that config values do not have:
- An owner — a named human, not a team alias
- A targeting rule — user IDs, plan tier, geography, percentage rollout
- An expiry date — the date after which the flag is technical debt
- A cleanup plan — which branch of the conditional wins when the flag is removed
- An audit trail — who flipped it, when, and why
The moment you drop a flag into settings.ENABLE_NEW_CHECKOUT = true, all five disappear. Six months later nobody knows if it is safe to remove, whether any customer still depends on the old path, or what happens if it flips off. That is feature flag technical debt, and it compounds silently.
An analogy: light switches vs. building wiring
Config is the building's wiring — the panel, the breakers, the gauge of the copper. You do not touch it casually. When you do, an electrician logs the change.
Feature flags are light switches installed for a specific renovation. They exist because you are mid-project and need the ability to turn off the half-built room while the rest of the house runs. Once the renovation is done, you rip the temporary switch out of the wall. If you leave it there for two years, someone eventually flips it during a party and the fuse box catches fire.
Same current running through both. Completely different governance.
A minimal worked example
Suppose you are rolling out a new billing engine. Here is what the wrong shape looks like:
# settings.py — everything in one bag
DB_HOST = "prod-db-01"
STRIPE_API_KEY = "sk_live_..."
ENABLE_NEW_BILLING = True
BILLING_ROLLOUT_PERCENT = 30
PAYMENT_KILL_SWITCH = False
EXPERIMENT_CHECKOUT_COPY = "variant_b"
Six problems in six lines. No owner on any flag. No expiry. No targeting logic — just a percent that someone will forget about. The kill switch is one typo away from being flipped by a config change. The experiment variant is global, not per-user. And a junior engineer editing DB_HOST is one file away from disabling billing for 70% of your customers.
Here is the right shape — three separate systems, each honest about what it is:
# config/ — long-lived, deploy-gated, code-reviewed
DB_HOST = "prod-db-01"
STRIPE_API_KEY = env("STRIPE_API_KEY")
# flags/ — feature flag service (LaunchDarkly, Unleash, Flagsmith, or a small in-house table)
flag(
key="new-billing-engine",
owner="priya@company.com",
created="2024-08-01",
expires="2024-11-01",
targeting=[
{"segment": "internal-users", "serve": True},
{"segment": "plan:enterprise", "serve": False},
{"default": {"rollout_percent": 30}}
],
cleanup="remove flag, keep new path"
)
# ops/ — kill switches with runbook links, alerting, and on-call ownership
kill_switch(
key="payments-circuit-breaker",
owner="platform-oncall",
runbook="https://wiki/incidents/payments",
default=False
)
# experiments/ — bound to an experiment ID, cohort, and metric
experiment(
id="checkout-copy-2024-q3",
variants=["control", "variant_a", "variant_b"],
assignment="sticky_by_user_id",
primary_metric="conversion_rate",
ends="2024-10-15"
)
Now the checkout code reads if flags.is_on("new-billing-engine", user). The kill switch is a separate call. The experiment is a separate call. A junior touching DB_HOST cannot brick billing. And every flag has a date on its tombstone.
Gotchas people hit
- Flag evaluation is now a network call. If your flag service is down, what do you serve? Every flag needs a documented default. Cache aggressively at the SDK level. Do not evaluate flags inside hot loops.
- Consistency across a single request. If you evaluate the same flag three times during one API call and it flips mid-request, you get a Frankenstein response. Evaluate once at the request boundary, pass the resolved value down.
- Flag debt is real debt. A flag that is 100% on for six months is dead code with a conditional wrapper. Enforce expiry. Fail CI when a flag is past its expiry date. Someone owns the cleanup PR.
- Do not use flags for entitlements. "Can this customer use the API?" is a billing question with a contract behind it, not a rollout decision. It belongs in your entitlements service.
- Audit everything. Every flag flip should log who, when, from where, and to what value. When something breaks at 2pm on a Tuesday, the first question is always "what changed?"
- Percentage rollouts need sticky hashing. If you roll out to 30% by random coin flip on every request, one user sees the flag on and off across sessions. Hash by user ID.
When to reach for a flag system vs. not
Use feature flags for: gradual rollouts of new code paths, dark launching, canary releases, kill switches on risky subsystems, and A/B tests that need statistical rigor.
Do not use feature flags for: anything permanent, plan entitlements, operational config, secrets, or values that change based on the environment (dev/staging/prod). Those are config. Ship them through your deployment pipeline where they get code review.
Skip a dedicated flag platform when: you are pre-launch with three engineers and one production customer. A well-structured internal table with owner, expiry, and targeting columns is fine. Adopt a platform when you have multiple teams shipping in parallel, or when a bad flip has real revenue consequences.
How CodeNicely can help
Untangling a shared config-and-flags mess is a specific kind of legacy work: you need to inventory every toggle, classify it (flag / config / kill switch / experiment / entitlement), and migrate each class to the right primitive without breaking behavior. We did exactly this kind of separation-of-concerns work on GimBooks, a YC-backed accounting SaaS where fast product iteration had collapsed several layers — billing rules, feature access, and rollout toggles — into overlapping code paths. The engagement was less about new features and more about restoring clean boundaries so the team could ship without stepping on themselves. If that is the shape of your current problem, our digital transformation and platform engineering teams handle this class of remediation as a defined scope of work, not an open-ended consulting retainer.
The one-line takeaway
Config describes how your system is wired. Flags describe decisions you have made about who sees what, until when. Store them separately, give every flag an owner and a death date, and treat flag debt as seriously as you treat any other production risk. The alternative is the incident you are debugging right now.
Frequently Asked Questions
What is the difference between feature flags and configuration?
Configuration describes long-lived system state — database hosts, API keys, timeouts — and changes through a deploy-adjacent, code-reviewed process. Feature flags are short-lived decisions with an owner, a target cohort, and an expiry date, meant to gate in-progress code and be removed once the feature is fully released. They look similar (both are key-value pairs) but have completely different governance and lifecycle.
Do I need a feature flag platform like LaunchDarkly, or can I build my own?
For a small team with a handful of flags and low blast radius, a simple internal table with columns for owner, expiry, targeting rules, and audit log is enough. Reach for a platform (LaunchDarkly, Unleash, Flagsmith, Statsig) when you have multiple teams flipping flags in parallel, need percentage rollouts with sticky hashing, or when a bad flip has direct revenue impact. The build-vs-buy line is usually crossed around 20–30 active flags or once non-engineers need to flip them safely.
How do I get rid of feature flag technical debt in an existing codebase?
Start with an inventory: list every flag, classify it (release toggle, kill switch, experiment, entitlement, or accidental config), and assign an owner and a target removal date to each. Anything at 100% on for more than a quarter is a candidate for deletion — remove the flag check, keep the new path, ship the cleanup PR. Enforce expiry going forward by failing CI when a flag passes its expiry date without action.
Should feature flags be evaluated in the frontend or backend?
Evaluate on the backend whenever the flag gates behavior that affects data or business logic, so you have one source of truth and a full audit trail. Frontend evaluation is fine for pure UI variations, but pass the resolved value from the backend when consistency matters (for example, if a checkout flow spans both). Evaluate each flag once per request at the boundary and pass the value down — do not re-evaluate inside functions.
My team wants to store feature flags in the database next to app settings. What is the harm?
The harm is that flags and settings have different reviewers, different change-approval bars, and different rollback semantics — but a shared table treats them identically. A junior editing a timeout value is one row away from disabling a paid feature for every enterprise customer. If you must share storage, at minimum separate them by table and put role-based access control on the flag table so only the flag owners can change targeting.
How does CodeNicely approach a feature-flag cleanup engagement?
We start with an inventory and classification pass, then design the target-state primitives (flags, config, kill switches, experiments, entitlements) that fit your team's scale, then migrate class by class with test coverage on the seams. For a scoped assessment of your codebase and a plan tailored to your team size and risk profile, contact CodeNicely for a personalized assessment.
Building something in SaaS?
CodeNicely partners with founders and tech teams to ship AI-native products that move metrics. Tell us about the problem you're solving.
Talk to our team_1751731246795-BygAaJJK.png)