Feature flags look harmless when a team first adopts them. One if statement, one config value, one safer release. Then six months pass and nobody remembers whether new_checkout_v2 is still needed.
That is why feature flag best practices matter more for small teams, not less. A five-person team doesn't have spare capacity for flag archaeology. If a flag ships without an owner, an expiry date, and a removal plan, it becomes another thing future you has to debug during an incident.
The goal is simple: ship continuously without turning production into a junk drawer.
Start with the type of flag
Martin Fowler's feature toggle guide separates flags by purpose: release toggles, experiment toggles, ops toggles, and permissioning toggles. That split is useful because each type needs a different cleanup habit.
A release flag exists to hide incomplete work until it is safe to expose. It should be short-lived. Once the feature is on for everyone and the old path is dead, the flag should disappear from the codebase.
An experiment flag supports A/B tests or rollout measurement. It needs a clear success metric and a decision date. If the team never decides, the experiment becomes a permanent fork in behavior.
An ops flag works like a control switch for production. A kill switch for an expensive recommendation panel can stay around because it protects the system during high load. It still needs documentation and periodic testing.
A permission flag controls access for a group of users, plans, roles, or accounts. These can be long-lived, but the rule should live in one place. Scattered permission checks become a quiet source of bugs.
Small teams get into trouble when every flag is treated the same. Before adding one, write its type in the ticket or pull request.
Keep flags close to release decisions
A feature flag should answer a release question. Can we deploy this code before the feature is public? Can we expose it to 5 percent of users? Can we turn it off if error rates climb?
If the flag answers none of those questions, it may be configuration, product entitlement, or business logic wearing the wrong name.
This matters because flags are read under stress. During a bad deploy, nobody wants to decode enable_new_flow_temp2. A useful flag name tells the team what it controls and why it exists.
Use names that include the area, feature, and purpose:
checkout.saved-cards.releasebilling.invoice-export.permissionsearch.relevance-v3.experimentfeed.recommendations.kill-switch
The exact pattern matters less than consistency. Unleash recommends unique naming patterns because reused or vague names can accidentally connect new work to old behavior. For a small team, a simple naming rule is enough.
Add ownership before the first merge
The easiest time to clean up a flag is before it exists.
Add four fields to every flag request:
- Owner: one person, not a team name.
- Type: release, experiment, ops, permission, or sunset.
- Expiry: the date when the flag must be reviewed.
- Removal condition: the event that lets the team delete it.
This can live in the flag description, the pull request body, or the issue tracker. Keep it boring. The point is to make cleanup visible without buying a new process.
For example:
Flag: checkout.saved-cards.release
Owner: Rina
Type: release
Expiry: 2026-07-21
Removal condition: saved cards enabled for 100 percent of users for 7 days with no rollbackA named owner changes the behavior of the whole team. Someone knows they are responsible for the final cleanup pull request. Someone will notice when the expiry date passes.
This fits naturally beside a code review checklist for small teams. Reviewers should ask, "Who removes this flag?" before approving the first merge.
Make cleanup part of done
Feature flag debt usually appears because the team treats rollout as done and cleanup as optional.
Change the definition of done:
- Code is deployed.
- The flag is rolled out or the feature is rejected.
- Old behavior is removed.
- The flag is archived or deleted from the flag system.
- Tests no longer cover dead branches.
LaunchDarkly's guidance says teams should plan removal during flag creation and include flags in the workflow's definition of done. That advice is boring in the best way. It turns cleanup from a heroic chore into a normal release step.
For release flags, create the cleanup ticket in the same sprint as the feature ticket. Don't wait until the rollout finishes. Waiting guarantees that the cleanup work loses context.
For experiment flags, attach the cleanup ticket to the experiment decision. If the experiment wins, remove the losing path. If it loses, remove the new path. If nobody can decide, escalate the decision instead of letting two versions run forever.
Avoid flag logic spread across the app
A common small-team mistake is checking the same flag in ten places.
At first it feels fast:
if (flags.checkoutSavedCards) {
showSavedCards()
}Then another check appears in validation. Another in analytics. Another in email templates. The flag stops being a release switch and becomes a second code path stitched through the product.
Prefer one decision point per feature boundary. Wrap the flag behind a small function or component so the rest of the app reads intent, not flag plumbing.
const canUseSavedCards = checkoutFlags.canUseSavedCards(user)
if (canUseSavedCards) {
renderSavedCards()
}This looks like extra work on day one. It pays back when the flag is removed. You delete the wrapper and one branch, rather than hunting every flag reference by hand.
Fowler also recommends separating the decision point from decision logic. In plain language, your feature code should not need to know how targeting rules work.
Test both paths, then delete one
Feature flags add test surface. That is the trade.
For short-lived release flags, test the old path and the new path while both exist. After rollout, delete the old path and delete the duplicate tests. Keeping both forever makes the suite slower and less clear.
For long-lived ops flags, test the fallback path on purpose. A kill switch that nobody has exercised for six months is just a comforting button on a dashboard.
This is also where CI matters. If your team already uses a GitHub Actions CI checklist, add one release question to it: which flag states must be tested before deploy?
You don't need a matrix for every flag combination. That explodes quickly. Pick the states that protect the release:
- Default off, for unreleased work.
- Default on, for the intended production path.
- Emergency off, for ops flags with fallback behavior.
The boring rule works: test what you may need to trust during a rollback.
Roll out in slices, not vibes
A rollout plan should fit on a small checklist.
For a release flag, use stages like this:
- Internal users only.
- One low-risk customer group.
- 5 percent of normal traffic.
- 25 percent after error rates stay normal.
- 100 percent after support and product signals look clean.
- Cleanup ticket starts.
The exact percentages can change. The habit is the point. Each step needs an observation window and a rollback condition.
Avoid "we'll watch it" as the monitoring plan. Watch what? Error rate, latency, conversion, support tickets, payment failures, logs for a specific exception? Pick two or three signals before the rollout begins.
For small teams, a lightweight release note in the pull request works well:
Rollout: internal -> 5% -> 25% -> 100%
Rollback: turn off checkout.saved-cards.release
Watch: payment error rate, checkout completion, card tokenization errors
Cleanup: remove old card entry path after 7 stable daysNow the release has memory. Anyone on call can understand the switch.
Keep permanent flags rare
Some flags are meant to last. Permissioning flags, plan-based access, and kill switches can be permanent.
Permanent does not mean ignored. It means the flag is part of product or operations design. It needs better naming, stronger tests, and clearer ownership than a temporary release flag.
Limit permanent flags to cases where the system really needs runtime control. If a flag only encodes business rules, consider moving that rule into the permission model or configuration layer.
Unleash warns that feature flags can make business logic harder to read when rules get scattered through the codebase. Small teams feel that pain fast because the same people who ship the feature also debug it later.
A good test is simple: if this flag vanished tomorrow, would the product lose a real control surface or would the code become easier to read?
Run a monthly flag review
A small team does not need a flag committee. It needs 30 minutes a month.
Open the flag dashboard or search the codebase for flag calls. For each flag, ask:
- Who owns it?
- Is it temporary or permanent?
- Has the expiry date passed?
- Is it serving the same value to every user?
- Can we remove the old path this week?
- Is the fallback still safe?
LaunchDarkly and Unleash both describe flag lifecycle states that help teams spot stale or inactive flags. If your tool has those states, use them. If it doesn't, a spreadsheet or issue label is still better than guessing.
The review should produce deletion pull requests, not only notes. Removing one old flag every month is enough to keep the mess from hardening.
Practical feature flag checklist for small teams
Use this before merging a flagged change:
- The flag name explains the product area and purpose.
- The flag type is written down.
- One owner is assigned.
- The expiry date is visible.
- The removal condition is clear.
- The default value is safe if the flag service fails.
- The rollout plan names stages and rollback conditions.
- Tests cover the states the team may need during release.
- The cleanup ticket exists before rollout starts.
- The pull request keeps flag checks near one decision boundary.
If any item feels too heavy, shorten the form. Don't delete the habit.
Sources
- Martin Fowler, Feature Toggles
- LaunchDarkly Docs, Reducing technical debt from feature flags
- Unleash Docs, Feature flag management: best practices
- Unleash Docs, Feature flags and lifecycle states



