# Flag lifecycle

Every flag passes through 6 phases. Skipping any phase creates debt.

```
request → design → ship → ramp → cleanup → archive
```

## Phase 1: Request

Triggered by an engineer or PM identifying a need.

**Required:**
- Flag name (kebab-case, descriptive: `new-checkout-flow` not `flag1`)
- Owner (named individual; not a team)
- Type (Release / Experiment / Operational / Permission)
- Justification (why a flag, not direct deploy?)
- Expected lifespan (days for Release, weeks for Experiment)

**Tool:** `assets/flag_request_template.md`

**Reject the request if:**
- It's a cosmetic change with no risk → ship via deploy
- It has no clear cleanup criteria → not a flag, refactor instead
- It duplicates an existing flag → reuse

## Phase 2: Design

Before writing code. Document decisions.

**Required artifacts:**
- Entry in `docs/feature-flags.md` (or your flag registry) with: name, owner, type, kill switch, dashboard URL
- Rollout plan generated by `rollout_planner.py`
- Kill-switch trigger and runbook
- Abort criteria with concrete thresholds

**Code location:**
- Single point of decision (not 5 `if (flag)` scattered)
- Use a strategy/feature-toggle pattern at module boundary

```python
# Good: one decision at module entry
if flags.is_enabled("new-checkout"):
    return new_checkout(request)
return legacy_checkout(request)

# Bad: flag check scattered through the function
def checkout(request):
    if flags.is_enabled("new-checkout"):
        validate_v2(request)
    else:
        validate_v1(request)
    if flags.is_enabled("new-checkout"):
        format_v2(request)
    else:
        format_v1(request)
    # ... many more
```

## Phase 3: Ship

Deploy with flag at **0% in production**, **100% in dev/staging**.

**Verification before merge:**
- [ ] `kill_switch_audit.py` passes
- [ ] Both branches (on/off) covered by tests
- [ ] Provider dashboard shows the flag at 0%
- [ ] Kill switch tested in staging (flip to ON, observe; flip to OFF, observe)
- [ ] Monitoring dashboard linked from flag-doc entry

**Common shipping mistakes:**
- Default-to-true in production (skip the safety wheels)
- Test only the new path; assume the old path still works
- Forget to update the flag-doc

## Phase 4: Ramp

Execute the rollout plan from `rollout_planner.py`. Hold each phase per `rollout_strategies.md`.

**Decision points:**
- After each phase: check abort criteria → hold | rollback | advance
- Communicate progress in team channel
- Update flag-doc with current percent and any abort events

## Phase 5: Cleanup

Once at 100% (or experiment concluded with a winner picked), remove the flag.

**Cleanup checklist:**
- [ ] Flag at 100% for ≥7 days (Release flags) OR test concluded (Experiment)
- [ ] Owner confirms no rollback risk
- [ ] Code change: delete the conditional, keep the new branch, delete the old branch
- [ ] Delete the flag in the provider dashboard
- [ ] Mark the flag-doc entry as ARCHIVED with date and PR link
- [ ] Add to CHANGELOG: "Removed feature flag: <name>"

**Common cleanup mistakes:**
- Removing the flag from code but forgetting the provider config (orphaned)
- Removing both branches (keep the new one)
- Not updating flag-doc (audit trail lost)
- Not running tests after removal (latent break)

## Phase 6: Archive

Move the flag-doc entry to an archive section. Keep the audit trail.

```markdown
## Archived

### new-checkout-flow [removed 2026-04-12, PR #1234]
- Owner: jane@team
- Type: Release
- Lifespan: 38 days from request to removal
- Outcome: Shipped at 100%; no incidents
```

## Lifecycle automation

| Phase | Tool / process |
|---|---|
| Request | `flag_request_template.md` filled in PR description |
| Design | `rollout_planner.py` output committed to PR |
| Ship | `kill_switch_audit.py` as pre-merge CI gate |
| Ramp | Provider dashboard execution; abort wired to alerts |
| Cleanup | Quarterly run of `flag_debt_scanner.py` |
| Archive | Manual (engineer cleanup PR) |

## SLAs by phase

| Phase | Max duration | Trigger if exceeded |
|---|---|---|
| Request → Design | 7 days | Owner ping |
| Design → Ship | 30 days | Owner ping; close request if stale |
| Ship → Ramp start | 7 days | Owner ping |
| Ramp → 100% (Release) | 30 days | Pause, review |
| 100% → Cleanup | 30 days | `flag_debt_scanner.py` flags it |
| Cleanup → Archive | 7 days | PR review reminder |

## Worked example

**Day 0:** Engineer files request: `new-search-relevance` Release flag, owner @bob, expected 21-day rollout.

**Day 2:** Design done. flag-doc entry created. `rollout_planner.py` output: ring strategy, 5 rings over 14 days. Kill-switch: any drop in CTR > 5%, set flag to 0% via provider API.

**Day 4:** Code shipped, flag at 0%. `kill_switch_audit.py` green. Smoke test passes.

**Day 5:** Ring 1 — 1% rollout. CTR within bounds. Hold 48h.

**Day 7:** Ring 2 — 5%. p99 latency +5% (within bounds). Hold 48h.

**Day 9:** Ring 3 — 25%. CTR +2% — winning. Hold 48h.

**Day 11:** Ring 4 — 50%. CTR +2.5%. Hold 48h.

**Day 13:** Ring 5 — 100%. Hold 7 days for stability.

**Day 20:** Cleanup PR opens — remove conditional, delete old branch.

**Day 21:** PR merged. Flag deleted in provider. flag-doc entry archived.

**Total elapsed: 21 days.** This is the target.

## When the lifecycle breaks

| Symptom | Diagnosis | Fix |
|---|---|---|
| Flag at 100% in code 6+ months | Cleanup phase skipped | Run `flag_debt_scanner.py` quarterly |
| Flag has no owner | Owner left; not reassigned | Assign to team's tech-debt owner; cleanup or transfer in 30 days |
| Two flags doing the same thing | Request phase missed dedup check | Consolidate; archive duplicate |
| Flag-doc entry missing | Design phase skipped | `kill_switch_audit.py` must be a CI gate |
| Flag flipped without rollout plan | Ramp phase skipped | Treat as incident; review cause |
