Feature Flags: Deploy Is Not Release
The four flag species and why conflating them causes misery, sticky percentage rollouts with one decision point, the staged rollout playbook with observability hooks, and the hygiene that prevents flag archaeology.
The single biggest mindset upgrade available to a shipping team fits in four words: deploy is not release. Deploying moves code to production; releasing exposes it to users — and feature flags are the mechanism that splits them. Once split, a cascade of good things becomes possible: merging half-finished work safely, rolling out to 1% before 100%, killing a misbehaving feature in seconds without a deploy, and A/B testing without a data science platform. Also possible: a codebase archaeologically layered with dead flags nobody dares remove. Both futures are real; the difference is discipline, and here it is.
The taxonomy — because "flag" is four different tools
- Release flags — hide in-progress work so trunk-based development works: merge daily behind the flag, integrate continuously, release when ready. Lifespan: days to weeks. Deleted at 100%.
- Ops flags / kill switches — permanent, deliberate circuit breakers around risky dependencies: "disable search suggestions", "pause push sends", "serve cached prices". Lifespan: forever, and reviewed like the safety equipment they are.
- Experiment flags — A/B variants with sticky assignment and metrics. Lifespan: until the experiment concludes, then deleted with a verdict recorded.
- Entitlement flags — plan-based feature access. These aren't flags at all; they're your billing model wearing a flag API, and they live in your tenant/plan configuration, permanently, under a different review regime.
Half of all flag-system misery comes from treating these four as one thing — the release flag that quietly became load-bearing entitlement logic is the classic.
The mechanics: targeting, stickiness, and the decision point
// The evaluation contract — context in, deterministic decision out
if (Feature::for($user)->active('new-checkout')) { ... }
// Behind it, rules evaluated in order:
// 1. kill switch off? → false, always wins
// 2. internal staff? → true (dogfooding lane)
// 3. tenant in beta list? → true (design-partner lane)
// 4. percentage rollout: hash(user_id + flag_name) % 100 < rollout_pct
Two properties are non-negotiable. Stickiness: the percentage gate hashes a stable identity — the same user gets the same answer on every request, or your 10% rollout becomes a UI that flickers between two checkouts per page load. One decision point per feature: evaluate the flag once, high in the request (or once per unit of work in jobs — never mid-job; a flag flipping between two steps of one workflow is a consistency bug you'll hunt for days), and pass the decision down. Scattering Feature::active() calls through six layers creates the flag whose "off" still leaves fragments running.
Tooling: Laravel Pennant or a small Redis/DB-backed service covers most teams (flags must flip without deploys — env vars fail the kill-switch test); LaunchDarkly/Unleash/Flagsmith earn their fee when you need audit trails, scheduled rollouts and a UI for non-engineers.
The rollout playbook
1. Ship dark merged, deployed, 0% — verified in prod by staff flag
2. Staff dogfood days of internal use; the cheapest bug reports exist here
3. 1% watch error rates + feature metrics, not vibes —
flag exposure tagged into logs/traces (see below)
4. 10% → 50% → 100% each step: soak, compare cohorts, proceed
5. DELETE THE FLAG the step that separates disciplined teams (next section)
At any step: kill switch → instant 0%, no deploy, incident over.
The observability hook in step 3 is the part teams skip: tag flag exposure into your traces and logs (flags: [new-checkout:on]) so "error rate of the 1% cohort vs control" is a query, not a guess. Without it, gradual rollout is gradual hoping.
Flag debt: the hygiene that keeps the lights on
Every conditional is a fork in your codebase's possibility space — ten boolean flags is theoretically 1,024 configurations, of which you test maybe three. The containment protocol: every release/experiment flag gets an owner and a removal ticket at creation (the ticket is created with the flag, not after); a monthly reaper report of flags at 100%/0% for 30+ days shames them out of existence; and stale-flag detection in CI (flags in code but absent from config, or vice versa) catches the drift. Cultural rule that does the most work: a feature isn't "done" while its release flag exists. Definition-of-done includes the deletion PR — which is also the moment the old code path gets deleted, the real payoff.
Do all this and flags become what they promised: deploys that are boring non-events, releases that are reversible decisions, and incidents that end with a toggle instead of a war room. Skip the hygiene and you get the other future — the one where nobody remembers what enable_new_flow_v2_final guards, and everyone's afraid to find out.
Introducing flags to a team — or excavating four years of them? Both are engagements I've run, and the second one comes with gloves.